diff --git a/Gruntfile.js b/Gruntfile.js
index ea6b69b..bb49281 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -6,6 +6,7 @@ module.exports = function( grunt ) {
 	BUILD_DIR  = 'build/',
 
 	BP_CSS = [
+		'bp-activity/css/*.css',
 		'bp-activity/admin/css/*.css',
 		'bp-core/admin/css/*.css',
 		'bp-core/css/*.css',
@@ -18,6 +19,7 @@ module.exports = function( grunt ) {
 	],
 
 	BP_JS = [
+		'bp-activity/js/*.js',
 		'bp-activity/admin/js/*.js',
 		'bp-core/js/*.js',
 		'bp-friends/js/*.js',
@@ -30,6 +32,8 @@ module.exports = function( grunt ) {
 	],
 
 	BP_EXCLUDED_JS = [
+		'!bp-core/js/jquery.atwho.js',
+		'!bp-core/js/jquery.caret.js',
 		'!bp-templates/bp-legacy/js/*.js'
 	];
 
diff --git a/src/bp-activity/bp-activity-actions.php b/src/bp-activity/bp-activity-actions.php
index d8e4c23..640a585 100644
--- a/src/bp-activity/bp-activity-actions.php
+++ b/src/bp-activity/bp-activity-actions.php
@@ -647,3 +647,28 @@ function bp_activity_setup_akismet() {
 	// Instantiate Akismet for BuddyPress
 	$bp->activity->akismet = new BP_Akismet();
 }
+
+/**
+ * AJAX endpoint for Suggestions API lookups.
+ *
+ * @since BuddyPress (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;
+	}
+
+	$results = bp_core_get_suggestions( array(
+		'term' => sanitize_text_field( $_GET['term'] ),
+		'type' => sanitize_text_field( $_GET['type'] ),
+	) );
+
+	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' );
diff --git a/src/bp-activity/bp-activity-cssjs.php b/src/bp-activity/bp-activity-cssjs.php
new file mode 100644
index 0000000..f4fa7f3
--- /dev/null
+++ b/src/bp-activity/bp-activity-cssjs.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * Activity component CSS/JS
+ *
+ * @package BuddyPress
+ * @subpackage ActivityScripts
+ */
+
+// Exit if accessed directly
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+/**
+ * Enqueue @mentions JS.
+ *
+ * @since BuddyPress (2.1)
+ */
+function bp_activity_mentions_script() {
+	if ( ! bp_activity_do_mentions() || ! bp_is_user_active() || ! ( bp_is_activity_component() || bp_is_blog_page() && is_singular() && comments_open() ) ) {
+		return;
+	}
+
+	$min  = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+	$file = is_rtl() ? "mentions-rtl{$min}.css" : "mentions{$min}.css";
+
+	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/{$file}", array(), bp_get_version() );
+
+	// Print a list of the current user's friends to the page for quicker @mentions lookups.
+	do_action( 'bp_activity_mentions_prime_results' );
+}
+add_action( 'bp_enqueue_scripts', 'bp_activity_mentions_script' );
+
+/**
+ * Enqueue @mentions JS in wp-admin.
+ *
+ * @since BuddyPress (2.1)
+ */
+function bp_activity_mentions_dashboard_script() {
+	if ( ! bp_activity_do_mentions() || ! bp_is_user_active() || ! is_admin() ) {
+		return;
+	}
+
+	// Special handling for New/Edit screens in wp-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  = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+	$file = is_rtl() ? "mentions-rtl{$min}.css" : "mentions{$min}.css";
+
+	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/{$file}", array(), bp_get_version() );
+
+	// Print a list of the current user's friends to the page for quicker @mentions lookups.
+	do_action( 'bp_activity_mentions_prime_results' );
+}
+add_action( 'bp_admin_enqueue_scripts', 'bp_activity_mentions_dashboard_script' );
\ No newline at end of file
diff --git a/src/bp-activity/bp-activity-loader.php b/src/bp-activity/bp-activity-loader.php
index c6fc540..bbf292d 100644
--- a/src/bp-activity/bp-activity-loader.php
+++ b/src/bp-activity/bp-activity-loader.php
@@ -47,6 +47,7 @@ class BP_Activity_Component extends BP_Component {
 	public function includes( $includes = array() ) {
 		// Files to include
 		$includes = array(
+			'cssjs',
 			'actions',
 			'screens',
 			'filters',
diff --git a/src/bp-activity/css/mentions-rtl.css b/src/bp-activity/css/mentions-rtl.css
new file mode 100644
index 0000000..7185ed6
--- /dev/null
+++ b/src/bp-activity/css/mentions-rtl.css
@@ -0,0 +1,78 @@
+.atwho-view {
+	background: #FFF;
+	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;
+	right: 0;
+	margin-top: 18px;
+	position: absolute;
+	top: 0;
+	z-index: 1000; /* >999 for wp-admin */
+}
+.atwho-view ul {
+	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-right: 10px;
+	width: 20px;
+}
+.atwho-view strong {
+	background: #EFEFEF;
+	font: bold;
+}
+.atwho-view .username strong {
+	color: #D54E21;
+}
+.atwho-view small {
+	color: #AAA;
+	float: left;
+	font-size: smaller;
+	font-weight: normal;
+	margin-right: 40px;
+}
+.atwho-view .cur {
+	background: rgba(239, 239, 239, 0.5);
+}
+
+@media (max-width: 900px) {
+	.atwho-view img {
+		float: right;
+		margin-right: 0;
+		margin-left: 10px;
+	}
+	.atwho-view small {
+		display: none;
+	}
+}
+@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;
+	}
+}
\ No newline at end of file
diff --git a/src/bp-activity/css/mentions.css b/src/bp-activity/css/mentions.css
new file mode 100644
index 0000000..9bc5134
--- /dev/null
+++ b/src/bp-activity/css/mentions.css
@@ -0,0 +1,78 @@
+.atwho-view {
+	background: #FFF;
+	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;
+	left: 0;
+	margin-top: 18px;
+	position: absolute;
+	top: 0;
+	z-index: 1000; /* >999 for wp-admin */
+}
+.atwho-view ul {
+	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-left: 10px;
+	width: 20px;
+}
+.atwho-view strong {
+	background: #EFEFEF;
+	font: bold;
+}
+.atwho-view .username strong {
+	color: #D54E21;
+}
+.atwho-view small {
+	color: #AAA;
+	float: right;
+	font-size: smaller;
+	font-weight: normal;
+	margin-left: 40px;
+}
+.atwho-view .cur {
+	background: rgba(239, 239, 239, 0.5);
+}
+
+@media (max-width: 900px) {
+	.atwho-view img {
+		float: left;
+		margin-left: 0;
+		margin-right: 10px;
+	}
+	.atwho-view small {
+		display: none;
+	}
+}
+@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;
+	}
+}
\ No newline at end of file
diff --git a/src/bp-activity/js/mentions.js b/src/bp-activity/js/mentions.js
new file mode 100644
index 0000000..ae36039
--- /dev/null
+++ b/src/bp-activity/js/mentions.js
@@ -0,0 +1,204 @@
+(function( $, undefined ) {
+	var mentionsQueryCache = [],
+		mentionsItem;
+
+	/**
+	 * 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 BuddyPress (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,
+			hide_without_suffix: true,
+			insert_tpl:          '</>${atwho-data-value}</>', // For contentEditable, the fake tags make jQuery insert a textNode.
+			limit:               10,
+			start_with_space:    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 BuddyPress (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 BuddyPress (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 BuddyPress (2.1.0)
+				 */
+				before_reposition: function( offset ) {
+					var $view = $( '#atwho-ground-' + this.id + ' .atwho-view' ),
+					caret     = this.$inputor.caret( 'offset', { iframe: $( '#content_ifr' )[0] } ).left,
+					move;
+
+					// If the caret is past horizontal half, then flip it, yo.
+					if ( caret > ( $( 'body' ).width() / 2 ) ) {
+						$view.addClass( 'flip' );
+						move = caret - offset.left - this.view.$el.width();
+					} else {
+						$view.removeClass( 'flip' );
+						move = caret - offset.left + 1;
+					}
+
+					offset.top  += 1;
+					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 BuddyPress (2.1.0)
+				 */
+				inserting_wrapper: function( $inputor, content, suffix ) {
+					var new_suffix = ( suffix === '' ) ? suffix : suffix || ' ';
+					return '' + content + new_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 BuddyPress (2.1.0)
+				 */
+				remote_filter: function( query, render_view ) {
+					var self = $( this );
+
+					mentionsItem = mentionsQueryCache[ query ];
+					if ( typeof mentionsItem === 'object' ) {
+						render_view( mentionsItem );
+						return;
+					}
+
+					if ( self.xhr ) {
+						self.xhr.abort();
+					}
+
+					self.xhr = $.getJSON( ajaxurl, { 'action': 'bp_get_suggestions', 'term': query, 'type': 'members' } )
+						/**
+						 * Success callback for the @suggestions lookup.
+						 *
+						 * @param {object} response Details of users matching the query.
+						 * @since BuddyPress (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 BuddyPress (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 BuddyPress (2.1.0)
+				 */
+				function( suggestion ) {
+					suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
+					return suggestion;
+				}
+			),
+
+			at:         '@',
+			search_key: 'search',
+			tpl:        '<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() {
+		var users = [];
+
+		if ( typeof window.BP_Suggestions === 'object' ) {
+			users = window.BP_Suggestions.friends || users;
+		}
+
+		// Activity/reply, post comments, dashboard post 'text' editor.
+		$( '.bp-suggestions, #comments form textarea, .wp-editor-area' ).bp_mentions( users );
+	});
+})( jQuery );
\ No newline at end of file
diff --git a/src/bp-core/bp-core-classes.php b/src/bp-core/bp-core-classes.php
index 5bbdf0b..ed9c260 100644
--- a/src/bp-core/bp-core-classes.php
+++ b/src/bp-core/bp-core-classes.php
@@ -368,17 +368,26 @@ class BP_User_Query {
 		// '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_like = bp_esc_like( $search_terms );
+			$search_terms = bp_esc_like( $search_terms );
 
 			if ( $search_wildcard === 'left' ) {
-				$search_terms_like = '%' . $search_terms_like;
+				$search_terms_nospace = '%' . $search_terms;
+				$search_terms_space   = '%' . $search_terms . ' %';
 			} elseif ( $search_wildcard === 'right' ) {
-				$search_terms_like = $search_terms_like . '%';
+				$search_terms_nospace =        $search_terms . '%';
+				$search_terms_space   = '% ' . $search_terms . '%';
 			} else {
-				$search_terms_like = '%' . $search_terms_like . '%';
+				$search_terms_nospace = '%' . $search_terms . '%';
+				$search_terms_space   = '%' . $search_terms . '%';
 			}
 
-			$sql['where']['search'] = $wpdb->prepare( "u.{$this->uid_name} IN ( SELECT ID FROM {$wpdb->users} WHERE ( user_login LIKE %s OR user_nicename LIKE %s ) )", $search_terms_like, $search_terms_like );
+			$sql['where']['search'] = $wpdb->prepare(
+				"u.{$this->uid_name} IN ( 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
+			);
 		}
 
 		// 'meta_key', 'meta_value' allow usermeta search
@@ -2566,7 +2575,7 @@ class BP_Members_Suggestions extends BP_Suggestions {
 	 * }
 	 */
 	protected $default_args = array(
-		'limit'        => 16,
+		'limit'        => 10,
 		'only_friends' => false,
 		'term'         => '',
 		'type'         => '',
@@ -2606,6 +2615,7 @@ class BP_Members_Suggestions extends BP_Suggestions {
 			'page'            => 1,
 			'per_page'        => $this->args['limit'],
 			'search_terms'    => $this->args['term'],
+			'search_wildcard' => is_rtl() ? 'left' : 'right',
 		);
 
 		// Only return matches of friends of this user.
diff --git a/src/bp-core/bp-core-cssjs.php b/src/bp-core/bp-core-cssjs.php
index b6fa396..c7f7011 100644
--- a/src/bp-core/bp-core-cssjs.php
+++ b/src/bp-core/bp-core-cssjs.php
@@ -19,10 +19,15 @@ function bp_core_register_common_scripts() {
 	$url = buddypress()->plugin_url . 'bp-core/js/';
 	
 	$scripts = apply_filters( 'bp_core_register_common_scripts', array(
+		// Legacy
 		'bp-confirm'        => array( 'file' => "{$url}confirm{$ext}",        'dependencies' => array( 'jquery' ) ),
 		'bp-widget-members' => array( 'file' => "{$url}widget-members{$ext}", 'dependencies' => array( 'jquery' ) ),
 		'bp-jquery-query'   => array( 'file' => "{$url}jquery-query{$ext}",   'dependencies' => array( 'jquery' ) ),
 		'bp-jquery-cookie'  => array( 'file' => "{$url}jquery-cookie{$ext}",  'dependencies' => array( 'jquery' ) ),
+
+		// 2.1
+		'jquery-caret' => array( 'file' => "{$url}jquery.caret{$ext}", 'dependencies' => array( 'jquery' ) ),
+		'jquery-atwho' => array( 'file' => "{$url}jquery.atwho{$ext}", 'dependencies' => array( 'jquery', 'jquery-caret' ) ),
 	) );
 
 	foreach ( $scripts as $id => $script ) {
diff --git a/src/bp-core/js/jquery.atwho.js b/src/bp-core/js/jquery.atwho.js
new file mode 100755
index 0000000..d5eefe3
--- /dev/null
+++ b/src/bp-core/js/jquery.atwho.js
@@ -0,0 +1,824 @@
+/*! jquery.atwho - v0.5.0 - 2014-07-14
+* Copyright (c) 2014 chord.luo <chord.luo@gmail.com>; 
+* homepage: http://ichord.github.com/At.js 
+* Licensed MIT
+*/
+
+(function() {
+  (function(factory) {
+    if (typeof define === 'function' && define.amd) {
+      return define(['jquery'], factory);
+    } else {
+      return factory(window.jQuery);
+    }
+  })(function($) {
+
+var $CONTAINER, Api, App, Controller, DEFAULT_CALLBACKS, KEY_CODE, Model, View,
+  __slice = [].slice;
+
+App = (function() {
+  function App(inputor) {
+    this.current_flag = null;
+    this.controllers = {};
+    this.alias_maps = {};
+    this.$inputor = $(inputor);
+    this.iframe = null;
+    this.setIframe();
+    this.listen();
+  }
+
+  App.prototype.setIframe = function(iframe) {
+    if (iframe) {
+      this.window = iframe.contentWindow;
+      this.document = iframe.contentDocument || this.window.document;
+      return this.iframe = iframe;
+    } else {
+      this.document = document;
+      this.window = window;
+      return this.iframe = null;
+    }
+  };
+
+  App.prototype.controller = function(at) {
+    var c, current, current_flag, _ref;
+    if (this.alias_maps[at]) {
+      current = this.controllers[this.alias_maps[at]];
+    } else {
+      _ref = this.controllers;
+      for (current_flag in _ref) {
+        c = _ref[current_flag];
+        if (current_flag === at) {
+          current = c;
+          break;
+        }
+      }
+    }
+    if (current) {
+      return current;
+    } else {
+      return this.controllers[this.current_flag];
+    }
+  };
+
+  App.prototype.set_context_for = function(at) {
+    this.current_flag = at;
+    return this;
+  };
+
+  App.prototype.reg = function(flag, setting) {
+    var controller, _base;
+    controller = (_base = this.controllers)[flag] || (_base[flag] = new Controller(this, flag));
+    if (setting.alias) {
+      this.alias_maps[setting.alias] = flag;
+    }
+    controller.init(setting);
+    return this;
+  };
+
+  App.prototype.listen = function() {
+    return this.$inputor.on('keyup.atwhoInner', (function(_this) {
+      return function(e) {
+        return _this.on_keyup(e);
+      };
+    })(this)).on('keydown.atwhoInner', (function(_this) {
+      return function(e) {
+        return _this.on_keydown(e);
+      };
+    })(this)).on('scroll.atwhoInner', (function(_this) {
+      return function(e) {
+        var _ref;
+        return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0;
+      };
+    })(this)).on('blur.atwhoInner', (function(_this) {
+      return function(e) {
+        var c;
+        if (c = _this.controller()) {
+          return c.view.hide(e, c.get_opt("display_timeout"));
+        }
+      };
+    })(this)).on('click.atwhoInner', (function(_this) {
+      return function(e) {
+        var _ref;
+        return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0;
+      };
+    })(this));
+  };
+
+  App.prototype.shutdown = function() {
+    var c, _, _ref;
+    _ref = this.controllers;
+    for (_ in _ref) {
+      c = _ref[_];
+      c.destroy();
+      delete this.controllers[_];
+    }
+    return this.$inputor.off('.atwhoInner');
+  };
+
+  App.prototype.dispatch = function() {
+    return $.map(this.controllers, (function(_this) {
+      return function(c) {
+        var delay;
+        if (delay = c.get_opt('delay')) {
+          clearTimeout(_this.delayedCallback);
+          return _this.delayedCallback = setTimeout(function() {
+            if (c.look_up()) {
+              return _this.set_context_for(c.at);
+            }
+          }, delay);
+        } else {
+          if (c.look_up()) {
+            return _this.set_context_for(c.at);
+          }
+        }
+      };
+    })(this));
+  };
+
+  App.prototype.on_keyup = 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:
+        $.noop();
+        break;
+      case KEY_CODE.P:
+      case KEY_CODE.N:
+        if (!e.ctrlKey) {
+          this.dispatch();
+        }
+        break;
+      default:
+        this.dispatch();
+    }
+  };
+
+  App.prototype.on_keydown = function(e) {
+    var view, _ref;
+    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:
+        if (!view.visible()) {
+          return;
+        }
+        e.preventDefault();
+        view.choose(e);
+        break;
+      default:
+        $.noop();
+    }
+  };
+
+  return App;
+
+})();
+
+Controller = (function() {
+  Controller.prototype.uid = function() {
+    return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
+  };
+
+  function Controller(app, at) {
+    this.app = app;
+    this.at = at;
+    this.$inputor = this.app.$inputor;
+    this.id = this.$inputor[0].id || this.uid();
+    this.setting = null;
+    this.query = null;
+    this.pos = 0;
+    this.cur_rect = null;
+    this.range = null;
+    $CONTAINER.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.call_default = function() {
+    var args, error, func_name;
+    func_name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+    try {
+      return DEFAULT_CALLBACKS[func_name].apply(this, args);
+    } catch (_error) {
+      error = _error;
+      return $.error("" + error + " Or maybe At.js doesn't have function " + func_name);
+    }
+  };
+
+  Controller.prototype.trigger = function(name, data) {
+    var alias, event_name;
+    if (data == null) {
+      data = [];
+    }
+    data.push(this);
+    alias = this.get_opt('alias');
+    event_name = alias ? "" + name + "-" + alias + ".atwho" : "" + name + ".atwho";
+    return this.$inputor.trigger(event_name, data);
+  };
+
+  Controller.prototype.callbacks = function(func_name) {
+    return this.get_opt("callbacks")[func_name] || DEFAULT_CALLBACKS[func_name];
+  };
+
+  Controller.prototype.get_opt = function(at, default_value) {
+    var e;
+    try {
+      return this.setting[at];
+    } catch (_error) {
+      e = _error;
+      return null;
+    }
+  };
+
+  Controller.prototype.content = function() {
+    if (this.$inputor.is('textarea, input')) {
+      return this.$inputor.val();
+    } else {
+      return this.$inputor.text();
+    }
+  };
+
+  Controller.prototype.catch_query = function() {
+    var caret_pos, content, end, query, start, subtext;
+    content = this.content();
+    caret_pos = this.$inputor.caret('pos', {
+      iframe: this.app.iframe
+    });
+    subtext = content.slice(0, caret_pos);
+    query = this.callbacks("matcher").call(this, this.at, subtext, this.get_opt('start_with_space'));
+    if (typeof query === "string" && query.length <= this.get_opt('max_len', 20)) {
+      start = caret_pos - query.length;
+      end = start + query.length;
+      this.pos = start;
+      query = {
+        'text': query,
+        'head_pos': start,
+        'end_pos': end
+      };
+      this.trigger("matched", [this.at, query.text]);
+    } else {
+      query = null;
+      this.view.hide();
+    }
+    return this.query = query;
+  };
+
+  Controller.prototype.rect = function() {
+    var c, scale_bottom;
+    if (!(c = this.$inputor.caret('offset', this.pos - 1, {
+      iframe: this.app.iframe
+    }))) {
+      return;
+    }
+    if (this.$inputor.attr('contentEditable') === 'true') {
+      c = (this.cur_rect || (this.cur_rect = c)) || c;
+    }
+    scale_bottom = this.app.document.selection ? 0 : 2;
+    return {
+      left: c.left,
+      top: c.top,
+      bottom: c.top + c.height + scale_bottom
+    };
+  };
+
+  Controller.prototype.reset_rect = function() {
+    if (this.$inputor.attr('contentEditable') === 'true') {
+      return this.cur_rect = null;
+    }
+  };
+
+  Controller.prototype.mark_range = function() {
+    if (this.$inputor.attr('contentEditable') === 'true') {
+      if (this.app.window.getSelection) {
+        this.range = this.app.window.getSelection().getRangeAt(0);
+      }
+      if (this.app.document.selection) {
+        return this.ie8_range = this.app.document.selection.createRange();
+      }
+    }
+  };
+
+  Controller.prototype.insert_content_for = function($li) {
+    var data, data_value, tpl;
+    data_value = $li.data('value');
+    tpl = this.get_opt('insert_tpl');
+    if (this.$inputor.is('textarea, input') || !tpl) {
+      return data_value;
+    }
+    data = $.extend({}, $li.data('item-data'), {
+      'atwho-data-value': data_value,
+      'atwho-at': this.at
+    });
+    return this.callbacks("tpl_eval").call(this, tpl, data);
+  };
+
+  Controller.prototype.insert = function(content, $li) {
+    var $inputor, content_node, pos, range, sel, source, start_str, text, wrapped_content;
+    $inputor = this.$inputor;
+    wrapped_content = this.callbacks('inserting_wrapper').call(this, $inputor, content, this.get_opt("suffix"));
+    if ($inputor.is('textarea, input')) {
+      source = $inputor.val();
+      start_str = source.slice(0, Math.max(this.query.head_pos - this.at.length, 0));
+      text = "" + start_str + wrapped_content + (source.slice(this.query['end_pos'] || 0));
+      $inputor.val(text);
+      $inputor.caret('pos', start_str.length + wrapped_content.length, {
+        iframe: this.app.iframe
+      });
+    } else if (range = this.range) {
+      pos = range.startOffset - (this.query.end_pos - this.query.head_pos) - this.at.length;
+      range.setStart(range.endContainer, Math.max(pos, 0));
+      range.setEnd(range.endContainer, range.endOffset);
+      range.deleteContents();
+      content_node = $(wrapped_content, this.app.document)[0];
+      range.insertNode(content_node);
+      range.setEndAfter(content_node);
+      range.collapse(false);
+      sel = this.app.window.getSelection();
+      sel.removeAllRanges();
+      sel.addRange(range);
+    } else if (range = this.ie8_range) {
+      range.moveStart('character', this.query.end_pos - this.query.head_pos - this.at.length);
+      range.pasteHTML(wrapped_content);
+      range.collapse(false);
+      range.select();
+    }
+    if (!$inputor.is(':focus')) {
+      $inputor.focus();
+    }
+    return $inputor.change();
+  };
+
+  Controller.prototype.render_view = function(data) {
+    var search_key;
+    search_key = this.get_opt("search_key");
+    data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), search_key);
+    return this.view.render(data.slice(0, this.get_opt('limit')));
+  };
+
+  Controller.prototype.look_up = function() {
+    var query, _callback;
+    if (!(query = this.catch_query())) {
+      return;
+    }
+    _callback = function(data) {
+      if (data && data.length > 0) {
+        return this.render_view(data);
+      } else {
+        return this.view.hide();
+      }
+    };
+    this.model.query(query.text, $.proxy(_callback, this));
+    return query;
+  };
+
+  return Controller;
+
+})();
+
+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 data, search_key, _remote_filter;
+    data = this.fetch();
+    search_key = this.context.get_opt("search_key");
+    data = this.context.callbacks('filter').call(this.context, query, data, search_key) || [];
+    _remote_filter = this.context.callbacks('remote_filter');
+    if (data.length > 0 || (!_remote_filter && data.length === 0)) {
+      return callback(data);
+    } else {
+      return _remote_filter.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("before_save").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;
+
+})();
+
+View = (function() {
+  function View(context) {
+    this.context = context;
+    this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
+    this.timeout_id = null;
+    this.context.$el.append(this.$el);
+    this.bind_event();
+  }
+
+  View.prototype.init = function() {
+    var id;
+    id = this.context.get_opt("alias") || this.context.at.charCodeAt(0);
+    return this.$el.attr({
+      'id': "at-view-" + id
+    });
+  };
+
+  View.prototype.destroy = function() {
+    return this.$el.remove();
+  };
+
+  View.prototype.bind_event = function() {
+    var $menu;
+    $menu = this.$el.find('ul');
+    return $menu.on('mouseenter.atwho-view', 'li', function(e) {
+      $menu.find('.cur').removeClass('cur');
+      return $(e.currentTarget).addClass('cur');
+    }).on('click', (function(_this) {
+      return function(e) {
+        _this.choose(e);
+        return e.preventDefault();
+      };
+    })(this));
+  };
+
+  View.prototype.visible = function() {
+    return this.$el.is(":visible");
+  };
+
+  View.prototype.choose = function(e) {
+    var $li, content;
+    if (($li = this.$el.find(".cur")).length) {
+      content = this.context.insert_content_for($li);
+      this.context.insert(this.context.callbacks("before_insert").call(this.context, content, $li), $li);
+      this.context.trigger("inserted", [$li, e]);
+      this.hide(e);
+    }
+    if (this.context.get_opt("hide_without_suffix")) {
+      return this.stop_showing = true;
+    }
+  };
+
+  View.prototype.reposition = function(rect) {
+    var offset, _ref;
+    if (rect.bottom + this.$el.height() - $(window).scrollTop() > $(window).height()) {
+      rect.bottom = rect.top - this.$el.height();
+    }
+    offset = {
+      left: rect.left,
+      top: rect.bottom
+    };
+    if ((_ref = this.context.callbacks("before_reposition")) != null) {
+      _ref.call(this.context, offset);
+    }
+    this.$el.offset(offset);
+    return this.context.trigger("reposition", [offset]);
+  };
+
+  View.prototype.next = function() {
+    var cur, next;
+    cur = this.$el.find('.cur').removeClass('cur');
+    next = cur.next();
+    if (!next.length) {
+      next = this.$el.find('li:first');
+    }
+    return next.addClass('cur');
+  };
+
+  View.prototype.prev = function() {
+    var cur, prev;
+    cur = this.$el.find('.cur').removeClass('cur');
+    prev = cur.prev();
+    if (!prev.length) {
+      prev = this.$el.find('li:last');
+    }
+    return prev.addClass('cur');
+  };
+
+  View.prototype.show = function() {
+    var rect;
+    if (this.stop_showing) {
+      this.stop_showing = false;
+      return;
+    }
+    this.context.mark_range();
+    if (!this.visible()) {
+      this.$el.show();
+      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.context.reset_rect();
+      this.$el.hide();
+      return this.context.trigger('hidden', [e]);
+    } else {
+      callback = (function(_this) {
+        return function() {
+          return _this.hide();
+        };
+      })(this);
+      clearTimeout(this.timeout_id);
+      return this.timeout_id = setTimeout(callback, time);
+    }
+  };
+
+  View.prototype.render = function(list) {
+    var $li, $ul, item, li, tpl, _i, _len;
+    if (!($.isArray(list) && list.length > 0)) {
+      this.hide();
+      return;
+    }
+    this.$el.find('ul').empty();
+    $ul = this.$el.find('ul');
+    tpl = this.context.get_opt('tpl');
+    for (_i = 0, _len = list.length; _i < _len; _i++) {
+      item = list[_i];
+      item = $.extend({}, item, {
+        'atwho-at': this.context.at
+      });
+      li = this.context.callbacks("tpl_eval").call(this.context, tpl, item);
+      $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.get_opt('highlight_first')) {
+      return $ul.find("li:first").addClass("cur");
+    }
+  };
+
+  return View;
+
+})();
+
+KEY_CODE = {
+  DOWN: 40,
+  UP: 38,
+  ESC: 27,
+  TAB: 9,
+  ENTER: 13,
+  CTRL: 17,
+  P: 80,
+  N: 78
+};
+
+DEFAULT_CALLBACKS = {
+  before_save: function(data) {
+    var item, _i, _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;
+  },
+  matcher: function(flag, subtext, should_start_with_space) {
+    var match, regexp;
+    flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+    if (should_start_with_space) {
+      flag = '(?:^|\\s)' + flag;
+    }
+    regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
+    match = regexp.exec(subtext);
+    if (match) {
+      return match[2] || match[1];
+    } else {
+      return null;
+    }
+  },
+  filter: function(query, data, search_key) {
+    var item, _i, _len, _results;
+    _results = [];
+    for (_i = 0, _len = data.length; _i < _len; _i++) {
+      item = data[_i];
+      if (~item[search_key].toLowerCase().indexOf(query.toLowerCase())) {
+        _results.push(item);
+      }
+    }
+    return _results;
+  },
+  remote_filter: null,
+  sorter: function(query, items, search_key) {
+    var item, _i, _len, _results;
+    if (!query) {
+      return items;
+    }
+    _results = [];
+    for (_i = 0, _len = items.length; _i < _len; _i++) {
+      item = items[_i];
+      item.atwho_order = item[search_key].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;
+    });
+  },
+  tpl_eval: function(tpl, map) {
+    var error;
+    try {
+      return tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+        return map[key];
+      });
+    } catch (_error) {
+      error = _error;
+      return "";
+    }
+  },
+  highlighter: function(li, query) {
+    var regexp;
+    if (!query) {
+      return li;
+    }
+    regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
+    return li.replace(regexp, function(str, $1, $2, $3) {
+      return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
+    });
+  },
+  before_insert: function(value, $li) {
+    return value;
+  },
+  inserting_wrapper: function($inputor, content, suffix) {
+    var new_suffix, wrapped_content;
+    new_suffix = suffix === "" ? suffix : suffix || " ";
+    if ($inputor.is('textarea, input')) {
+      return '' + content + new_suffix;
+    } else if ($inputor.attr('contentEditable') === 'true') {
+      new_suffix = suffix === "" ? suffix : suffix || "&nbsp;";
+      if (/firefox/i.test(navigator.userAgent)) {
+        wrapped_content = "<span>" + content + new_suffix + "</span>";
+      } else {
+        suffix = "<span contenteditable='false'>" + new_suffix + "<span>";
+        wrapped_content = "<span contenteditable='false'>" + content + suffix + "</span>";
+      }
+      if (this.app.document.selection) {
+        wrapped_content = "<span contenteditable='true'>" + content + "</span>";
+      }
+      return wrapped_content;
+    }
+  }
+};
+
+Api = {
+  load: function(at, data) {
+    var c;
+    if (c = this.controller(at)) {
+      return c.model.load(data);
+    }
+  },
+  setIframe: function(iframe) {
+    this.setIframe(iframe);
+    return null;
+  },
+  run: function() {
+    return this.dispatch();
+  },
+  destroy: function() {
+    this.shutdown();
+    return this.$inputor.data('atwho', null);
+  }
+};
+
+$CONTAINER = $("<div id='atwho-container'></div>");
+
+$.fn.atwho = function(method) {
+  var result, _args;
+  _args = arguments;
+  $('body').append($CONTAINER);
+  result = null;
+  this.filter('textarea, input, [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.caret");
+    }
+  });
+  return result || this;
+};
+
+$.fn.atwho["default"] = {
+  at: void 0,
+  alias: void 0,
+  data: null,
+  tpl: "<li data-value='${atwho-at}${name}'>${name}</li>",
+  insert_tpl: "<span id='${id}'>${atwho-data-value}</span>",
+  callbacks: DEFAULT_CALLBACKS,
+  search_key: "name",
+  suffix: void 0,
+  hide_without_suffix: false,
+  start_with_space: true,
+  highlight_first: true,
+  limit: 5,
+  max_len: 20,
+  display_timeout: 300,
+  delay: null
+};
+
+  });
+}).call(this);
diff --git a/src/bp-core/js/jquery.atwho.txt b/src/bp-core/js/jquery.atwho.txt
new file mode 100755
index 0000000..36cd1c1
--- /dev/null
+++ b/src/bp-core/js/jquery.atwho.txt
@@ -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.
diff --git a/src/bp-core/js/jquery.caret.js b/src/bp-core/js/jquery.caret.js
new file mode 100755
index 0000000..caa7876
--- /dev/null
+++ b/src/bp-core/js/jquery.caret.js
@@ -0,0 +1,366 @@
+/*
+  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 内的插入符
+只实现了获得插入符在文本框中的位置，我设置
+插入符的位置.
+*/
+
+
+(function() {
+  (function(factory) {
+    if (typeof define === 'function' && define.amd) {
+      return define(['jquery'], factory);
+    } else {
+      return factory(window.jQuery);
+    }
+  })(function($) {
+    "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) {
+        return this.domInputor;
+      };
+
+      EditableCaret.prototype.getIEPosition = function() {
+        return $.noop();
+      };
+
+      EditableCaret.prototype.getPosition = function() {
+        return $.noop();
+      };
+
+      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;
+        if (oWindow.getSelection && (range = this.range())) {
+          if (range.endOffset - 1 < 0) {
+            return null;
+          }
+          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();
+        } else if (oDocument.selection) {
+          offset = this.getOldIEOffset();
+        }
+        if (offset && !oFrame) {
+          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) {
+          return value.replace(/</g, '&lt').replace(/>/g, '&gt').replace(/`/g, '&#96').replace(/"/g, '&quot').replace(/\r\n|\r|\n/g, "<br />");
+        };
+        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 iOffset, offset;
+        offset = this.getOffset(pos);
+        if (oFrame) {
+          iOffset = $(oFrame).offset();
+          offset.top += iOffset.top;
+          offset.left += iOffset.left;
+        }
+        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;
+    return $.fn.caret.apis = methods;
+  });
+
+}).call(this);
diff --git a/src/bp-core/js/jquery.caret.txt b/src/bp-core/js/jquery.caret.txt
new file mode 100755
index 0000000..36cd1c1
--- /dev/null
+++ b/src/bp-core/js/jquery.caret.txt
@@ -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.
diff --git a/src/bp-friends/bp-friends-functions.php b/src/bp-friends/bp-friends-functions.php
index 5f797c7..9970c63 100644
--- a/src/bp-friends/bp-friends-functions.php
+++ b/src/bp-friends/bp-friends-functions.php
@@ -566,3 +566,41 @@ function 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.
+ *
+ * @see bp_activity_mentions_script()
+ */
+function bp_friends_prime_mentions_results() {
+	if ( ! bp_activity_do_mentions() || ! bp_is_user_active() ) {
+		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 ) );
+		$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' );
\ No newline at end of file
diff --git a/src/bp-groups/bp-groups-classes.php b/src/bp-groups/bp-groups-classes.php
index 02e26be..7121ea4 100644
--- a/src/bp-groups/bp-groups-classes.php
+++ b/src/bp-groups/bp-groups-classes.php
@@ -4387,6 +4387,7 @@ class BP_Groups_Member_Suggestions extends BP_Members_Suggestions {
 			'page'            => 1,
 			'per_page'        => $this->args['limit'],
 			'search_terms'    => $this->args['term'],
+			'search_wildcard' => 'right',
 		);
 
 		// Only return matches of friends of this user.
diff --git a/src/bp-templates/bp-legacy/buddypress/activity/entry.php b/src/bp-templates/bp-legacy/buddypress/activity/entry.php
index 24e296b..2d759b9 100644
--- a/src/bp-templates/bp-legacy/buddypress/activity/entry.php
+++ b/src/bp-templates/bp-legacy/buddypress/activity/entry.php
@@ -97,7 +97,7 @@
 					<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">
-							<textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
+							<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(); ?>" />
diff --git a/src/bp-templates/bp-legacy/buddypress/activity/post-form.php b/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
index acf7368..a45213a 100644
--- a/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
+++ b/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
@@ -27,7 +27,7 @@
 
 	<div id="whats-new-content">
 		<div id="whats-new-textarea">
-			<textarea name="whats-new" id="whats-new" cols="50" rows="10"><?php if ( isset( $_GET['r'] ) ) : ?>@<?php echo esc_textarea( $_GET['r'] ); ?> <?php endif; ?></textarea>
+			<textarea class="bp-suggestions" name="whats-new" id="whats-new" cols="50" rows="10"><?php if ( isset( $_GET['r'] ) ) : ?>@<?php echo esc_textarea( $_GET['r'] ); ?> <?php endif; ?></textarea>
 		</div>
 
 		<div id="whats-new-options">
diff --git a/src/bp-xprofile/bp-xprofile-functions.php b/src/bp-xprofile/bp-xprofile-functions.php
index 7cce675..8cedbb0 100644
--- a/src/bp-xprofile/bp-xprofile-functions.php
+++ b/src/bp-xprofile/bp-xprofile-functions.php
@@ -590,22 +590,29 @@ function bp_xprofile_bp_user_query_search( $sql, BP_User_Query $query ) {
 
 	$bp = buddypress();
 
-	$search_terms_clean = esc_sql( esc_sql( $query->query_vars['search_terms'] ) );
+	$search_terms_clean = bp_esc_like( $query->query_vars['search_terms'] );
 
 	if ( $query->query_vars['search_wildcard'] === 'left' ) {
-		$search_terms_clean = '%' . $search_terms_clean;
+		$search_terms_nospace = '%' . $search_terms_clean;
+		$search_terms_space   = '%' . $search_terms_clean . ' %';
 	} elseif ( $query->query_vars['search_wildcard'] === 'right' ) {
-		$search_terms_clean = $search_terms_clean . '%';
+		$search_terms_nospace =        $search_terms_clean . '%';
+		$search_terms_space   = '% ' . $search_terms_clean . '%';
 	} else {
-		$search_terms_clean = '%' . $search_terms_clean . '%';
+		$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
+	$search_xprofile = $wpdb->prepare(
+		"u.{$query->uid_name} IN ( SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE %s OR value LIKE %s )",
+		$search_terms_nospace,
+		$search_terms_space
+	);
+
 	$search_core     = $sql['where']['search'];
-	$search_xprofile = "u.{$query->uid_name} IN ( SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE '{$search_terms_clean}' )";
 	$search_combined = "( {$search_xprofile} OR {$search_core} )";
-
 	$sql['where']['search'] = $search_combined;
 
 	return $sql;
