<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[BuddyPress][7860] trunk: Migrate user 'last_activity' data from usermeta to the activity table</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://buddypress.trac.wordpress.org/changeset/7860">7860</a></dd>
<dt>Author</dt> <dd>boonebgorges</dd>
<dt>Date</dt> <dd>2014-02-12 21:27:20 +0000 (Wed, 12 Feb 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Migrate user 'last_activity' data from usermeta to the activity table

Storing last_activity in usermeta caused severe bottlenecks on sites with
large user bases. The usermeta table has a tendency to get bloated. Its
option_value column is not indexed, and even if it were, it would not be
indexed properly for the kind of chronological sorting that BuddyPress was
using it for.

This changeset refactors all core last_activity user functionality, so that
the data is stored in the wp_bp_activity table (even when the activity
component is disabled).

For backward compatibility with plugins that reference last_activity metadata
entries, all last_activity data is retained in wp_usermeta, and new data will
be mirrored there until further notice.

See <a href="http://buddypress.trac.wordpress.org/ticket/5128">#5128</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkbpactivitybpactivityclassesphp">trunk/bp-activity/bp-activity-classes.php</a></li>
<li><a href="#trunkbpcoreadminbpcoreschemaphp">trunk/bp-core/admin/bp-core-schema.php</a></li>
<li><a href="#trunkbpcorebpcoreclassesphp">trunk/bp-core/bp-core-classes.php</a></li>
<li><a href="#trunkbpcorebpcorefunctionsphp">trunk/bp-core/bp-core-functions.php</a></li>
<li><a href="#trunkbpcorebpcoreupdatephp">trunk/bp-core/bp-core-update.php</a></li>
<li><a href="#trunkbpfriendsbpfriendsclassesphp">trunk/bp-friends/bp-friends-classes.php</a></li>
<li><a href="#trunkbploaderphp">trunk/bp-loader.php</a></li>
<li><a href="#trunkbpmembersbpmembersfunctionsphp">trunk/bp-members/bp-members-functions.php</a></li>
<li><a href="#trunkbpmembersbpmembersloaderphp">trunk/bp-members/bp-members-loader.php</a></li>
<li><a href="#trunktestsincludestestcasephp">trunk/tests/includes/testcase.php</a></li>
<li><a href="#trunkteststestcasescoreclassbpcoreuserphp">trunk/tests/testcases/core/class-bp-core-user.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkbpactivitybpactivityclassesphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-activity/bp-activity-classes.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-activity/bp-activity-classes.php        2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-activity/bp-activity-classes.php   2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -361,6 +361,12 @@
</span><span class="cx">                  $where_conditions[] = "a.type != 'activity_comment'";
</span><span class="cx">          }
</span><span class="cx"> 
</span><ins>+               // Exclude 'last_activity' items unless the 'action' filter has
+               // been explicitly set
+               if ( empty( $filter['object'] ) ) {
+                       $where_conditions[] = "a.type != 'last_activity'";
+               }
+
</ins><span class="cx">           // Filter the where conditions
</span><span class="cx">          $where_conditions = apply_filters( 'bp_activity_get_where_conditions', $where_conditions, $r, $select_sql, $from_sql, $join_sql );
</span><span class="cx"> 
</span><span class="lines">@@ -1062,13 +1068,15 @@
</span><span class="cx">          global $wpdb;
</span><span class="cx"> 
</span><span class="cx">          // split items at the comma
</span><del>-               $items_dirty = explode( ',', $items );
</del><ins>+                if ( ! is_array( $items ) ) {
+                       $items = explode( ',', $items );
+               }
</ins><span class="cx"> 
</span><span class="cx">          // array of prepared integers or quoted strings
</span><span class="cx">          $items_prepared = array();
</span><span class="cx"> 
</span><span class="cx">          // clean up and format each item
</span><del>-               foreach ( $items_dirty as $item ) {
</del><ins>+                foreach ( $items as $item ) {
</ins><span class="cx">                   // clean up the string
</span><span class="cx">                  $item = trim( $item );
</span><span class="cx">                  // pass everything through prepare for security and to safely quote strings
</span><span class="lines">@@ -1120,7 +1128,7 @@
</span><span class="cx"> 
</span><span class="cx">          if ( !empty( $filter_array['action'] ) ) {
</span><span class="cx">                  $action_sql = BP_Activity_Activity::get_in_operator_sql( 'a.type', $filter_array['action'] );
</span><del>-                       if ( !empty( $action_sql ) )
</del><ins>+                        if ( ! empty( $action_sql ) )
</ins><span class="cx">                           $filter_sql[] = $action_sql;
</span><span class="cx">          }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkbpcoreadminbpcoreschemaphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-core/admin/bp-core-schema.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-core/admin/bp-core-schema.php   2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-core/admin/bp-core-schema.php      2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -23,14 +23,14 @@
</span><span class="cx">  if ( empty( $active_components ) )
</span><span class="cx">          $active_components = apply_filters( 'bp_active_components', bp_get_option( 'bp-active-components' ) );
</span><span class="cx"> 
</span><ins>+       // Activity Streams
+       // Install tables even when inactive, to store last_activity data
+       bp_core_install_activity_streams();
+
</ins><span class="cx">   // Notifications
</span><span class="cx">  if ( !empty( $active_components['notifications'] ) )
</span><span class="cx">          bp_core_install_notifications();
</span><span class="cx"> 
</span><del>-       // Activity Streams
-       if ( !empty( $active_components['activity'] ) )
-               bp_core_install_activity_streams();
-
</del><span class="cx">   // Friend Connections
</span><span class="cx">  if ( !empty( $active_components['friends'] ) )
</span><span class="cx">          bp_core_install_friends();
</span></span></pre></div>
<a id="trunkbpcorebpcoreclassesphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-core/bp-core-classes.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-core/bp-core-classes.php        2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-core/bp-core-classes.php   2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -245,10 +245,10 @@
</span><span class="cx">                  // number of minutes used as an interval
</span><span class="cx">                  case 'online' :
</span><span class="cx">                          $this->uid_name = 'user_id';
</span><del>-                               $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$wpdb->usermeta} u";
-                               $sql['where'][] = $wpdb->prepare( "u.meta_key = %s", bp_get_user_meta_key( 'last_activity' ) );
-                               $sql['where'][] = $wpdb->prepare( "u.meta_value >= DATE_SUB( UTC_TIMESTAMP(), INTERVAL %d MINUTE )", apply_filters( 'bp_user_query_online_interval', 15 ) );
-                               $sql['orderby'] = "ORDER BY u.meta_value";
</del><ins>+                                $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$bp->members->table_name_last_activity} u";
+                               $sql['where'][] = $wpdb->prepare( "u.component = %s AND u.type = 'last_activity'", buddypress()->members->id );
+                               $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";
</ins><span class="cx">                           $sql['order']   = "DESC";
</span><span class="cx"> 
</span><span class="cx">                          break;
</span><span class="lines">@@ -259,8 +259,8 @@
</span><span class="cx">                  case 'newest' :
</span><span class="cx">                  case 'random' :
</span><span class="cx">                          $this->uid_name = 'user_id';
</span><del>-                               $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$wpdb->usermeta} u";
-                               $sql['where'][] = $wpdb->prepare( "u.meta_key = %s", bp_get_user_meta_key( 'last_activity' ) );
</del><ins>+                                $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$bp->members->table_name_last_activity} u";
+                               $sql['where'][] = $wpdb->prepare( "u.component = %s AND u.type = 'last_activity'", buddypress()->members->id );
</ins><span class="cx"> 
</span><span class="cx">                          if ( 'newest' == $type ) {
</span><span class="cx">                                  $sql['orderby'] = "ORDER BY u.user_id";
</span><span class="lines">@@ -268,7 +268,7 @@
</span><span class="cx">                          } else if ( 'random' == $type ) {
</span><span class="cx">                                  $sql['orderby'] = "ORDER BY rand()";
</span><span class="cx">                          } else {
</span><del>-                                       $sql['orderby'] = "ORDER BY u.meta_value";
</del><ins>+                                        $sql['orderby'] = "ORDER BY u.date_recorded";
</ins><span class="cx">                                   $sql['order'] = "DESC";
</span><span class="cx">                          }
</span><span class="cx"> 
</span><span class="lines">@@ -538,6 +538,8 @@
</span><span class="cx">          // Turn user ID's into a query-usable, comma separated value
</span><span class="cx">          $user_ids_sql = implode( ',', wp_parse_id_list( $this->user_ids ) );
</span><span class="cx"> 
</span><ins>+               $bp = buddypress();
+
</ins><span class="cx">           /**
</span><span class="cx">           * Use this action to independently populate your own custom extras.
</span><span class="cx">           *
</span><span class="lines">@@ -553,13 +555,22 @@
</span><span class="cx">           */
</span><span class="cx">          do_action_ref_array( 'bp_user_query_populate_extras', array( $this, $user_ids_sql ) );
</span><span class="cx"> 
</span><ins>+               // Fetch last_active data from the activity table
+               $last_activities = BP_Core_User::get_last_activity( $this->user_ids );
+
+               if ( ! empty( $last_activities ) ) {
+                       foreach ( $last_activities as $la_user => $la_value ) {
+                               if ( isset( $this->results[ $la_user ] ) ) {
+                                       $this->results[ $la_user ]->last_activity = $la_value['date_recorded'];
+                               }
+                       }
+               }
+
</ins><span class="cx">           // Fetch usermeta data
</span><span class="cx">          // We want the three following pieces of info from usermeta:
</span><span class="cx">          // - friend count
</span><del>-               // - last activity
</del><span class="cx">           // - latest update
</span><span class="cx">          $total_friend_count_key = bp_get_user_meta_key( 'total_friend_count' );
</span><del>-               $last_activity_key      = bp_get_user_meta_key( 'last_activity'      );
</del><span class="cx">           $bp_latest_update_key   = bp_get_user_meta_key( 'bp_latest_update'   );
</span><span class="cx"> 
</span><span class="cx">          // total_friend_count must be set for each user, even if its
</span><span class="lines">@@ -569,7 +580,7 @@
</span><span class="cx">          }
</span><span class="cx"> 
</span><span class="cx">          // Create, prepare, and run the seperate usermeta query
</span><del>-               $user_metas = $wpdb->get_results( $wpdb->prepare( "SELECT user_id, meta_key, meta_value FROM {$wpdb->usermeta} WHERE meta_key IN (%s,%s,%s) AND user_id IN ({$user_ids_sql})", $total_friend_count_key, $last_activity_key, $bp_latest_update_key ) );
</del><ins>+                $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 ) );
</ins><span class="cx"> 
</span><span class="cx">          // The $members_template global expects the index key to be different
</span><span class="cx">          // from the meta_key in some cases, so we rejig things here.
</span><span class="lines">@@ -579,10 +590,6 @@
</span><span class="cx">                                  $key = 'total_friend_count';
</span><span class="cx">                                  break;
</span><span class="cx"> 
</span><del>-                               case $last_activity_key :
-                                       $key = 'last_activity';
-                                       break;
-
</del><span class="cx">                           case $bp_latest_update_key :
</span><span class="cx">                                  $key = 'latest_update';
</span><span class="cx">                                  break;
</span><span class="lines">@@ -1297,6 +1304,163 @@
</span><span class="cx"> 
</span><span class="cx">          return $user;
</span><span class="cx">  }
</span><ins>+
+       /**
+        * Get last activity data for a user or set of users.
+        *
+        * @param int|array User IDs or multiple user IDs.
+        * @return array
+        */
+       public static function get_last_activity( $user_id ) {
+               global $wpdb;
+
+               if ( is_array( $user_id ) ) {
+                       $user_ids = wp_parse_id_list( $user_id );
+               } else {
+                       $user_ids = array( absint( $user_id ) );
+               }
+
+               if ( empty( $user_ids ) ) {
+                       return false;
+               }
+
+               $bp = buddypress();
+
+               $user_ids_sql = implode( ',', $user_ids );
+               $user_count   = count( $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 ) );
+
+               // Re-key
+               $retval = array();
+               foreach ( $last_activities as $last_activity ) {
+                       $retval[ $last_activity->user_id ] = array(
+                               'user_id'       => $last_activity->user_id,
+                               'date_recorded' => $last_activity->date_recorded,
+                               'activity_id'   => $last_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
+        *
+        * @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;
+
+               $existing = self::get_last_activity( $user_id );
+
+               if ( ! empty( $existing ) ) {
+                       $data = array(
+                               'date_recorded' => $time,
+                       );
+
+                       $data_format = array(
+                               '%s',
+                       );
+
+                       $where = array(
+                       );
+
+                       $where_format = array(
+                               '%d',
+                       );
+
+                       $updated = $wpdb->update(
+                               $table_name,
+
+                               // Data to update
+                               array(
+                                       'date_recorded' => $time,
+                               ),
+
+                               // WHERE
+                               array(
+                                       'id' => $existing[ $user_id ]['activity_id'],
+                               ),
+
+                               // Data sanitization format
+                               array(
+                                       '%s',
+                               ),
+
+                               // WHERE sanitization format
+                               array(
+                                       '%d',
+                               )
+                       );
+               } else {
+                       $updated = $wpdb->insert(
+                               $table_name,
+
+                               // Data
+                               array(
+                                       'user_id'       => $user_id,
+                                       'component'     => buddypress()->members->id,
+                                       'type'          => 'last_activity',
+                                       'date_recorded' => $time,
+                               ),
+
+                               // Data sanitization format
+                               array(
+                                       '%d',
+                                       '%s',
+                                       '%s',
+                                       '%s',
+                               )
+                       );
+               }
+
+               return $updated;
+       }
+
+       /**
+        * Delete a user's last_activity value.
+        *
+        * @since 2.0
+        *
+        * @param int $user_id
+        * @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 ) ) {
+                       return false;
+               }
+
+               $deleted = $wpdb->delete(
+                       buddypress()->members->table_name_last_activity,
+
+                       // WHERE
+                       array(
+                               'id' => $existing[ $user_id ]['activity_id'],
+                       ),
+
+                       // WHERE sanitization format
+                       array(
+                               '%s',
+                       )
+               );
+
+               return $deleted;
+       }
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkbpcorebpcorefunctionsphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-core/bp-core-functions.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-core/bp-core-functions.php      2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-core/bp-core-functions.php 2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -1053,10 +1053,10 @@
</span><span class="cx">  *
</span><span class="cx">  * Plugin authors should use BP's _user_meta() functions, which bakes in
</span><span class="cx">  * bp_get_user_meta_key():
</span><del>- *    $last_active = bp_get_user_meta( $user_id, 'last_activity', true );
</del><ins>+ *    $friend_count = bp_get_user_meta( $user_id, 'total_friend_count', true );
</ins><span class="cx">  * If you must use WP's _user_meta() functions directly for some reason, you
</span><span class="cx">  * should use this function to determine the $key parameter, eg
</span><del>- *    $last_active = get_user_meta( $user_id, bp_get_user_meta_key( 'last_activity' ), true );
</del><ins>+ *    $friend_count = get_user_meta( $user_id, bp_get_user_meta_key( 'total_friend_count' ), true );
</ins><span class="cx">  * If using the WP functions, do not not hardcode your meta keys.
</span><span class="cx">  *
</span><span class="cx">  * @since BuddyPress (1.5.0)
</span></span></pre></div>
<a id="trunkbpcorebpcoreupdatephp"></a>
<div class="modfile"><h4>Modified: trunk/bp-core/bp-core-update.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-core/bp-core-update.php 2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-core/bp-core-update.php    2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -230,6 +230,11 @@
</span><span class="cx">          if ( $raw_db_version < 7731 ) {
</span><span class="cx">                  bp_update_to_1_9_2();
</span><span class="cx">          }
</span><ins>+
+               // 2.0
+               if ( $raw_db_version < 7859 ) {
+                       bp_update_to_2_0();
+               }
</ins><span class="cx">   }
</span><span class="cx"> 
</span><span class="cx">  /** All done! *************************************************************/
</span><span class="lines">@@ -328,6 +333,38 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><ins>+ * 2.0 update routine.
+ *
+ * - Ensure that the activity tables are installed, for last_activity storage.
+ * - Migrate last_activity data from usermeta to activity table
+ */
+function bp_update_to_2_0() {
+       global $wpdb;
+
+       // Install activity tables
+       bp_core_install_activity_streams();
+
+       $bp = buddypress();
+
+       // Migrate data
+       // The "NOT IN" clause prevents duplicates
+       $sql = "INSERT INTO {$bp->members->table_name_last_activity} (`user_id`, `component`, `type`, `date_recorded` ) (
+                 SELECT user_id, '{$bp->members->id}' as component, 'last_activity' as type, meta_value AS date_recorded
+                 FROM {$wpdb->usermeta}
+                 WHERE
+                   meta_key = 'last_activity'
+                   AND
+                   user_id NOT IN (
+                     SELECT user_id
+                     FROM {$bp->members->table_name_last_activity}
+                     WHERE component = '{$bp->members->id}' AND type = 'last_activity'
+                   )
+       );";
+
+       $wpdb->query( $sql );
+}
+
+/**
</ins><span class="cx">  * Redirect user to BP's What's New page on first page load after activation.
</span><span class="cx">  *
</span><span class="cx">  * @since BuddyPress (1.7.0)
</span></span></pre></div>
<a id="trunkbpfriendsbpfriendsclassesphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-friends/bp-friends-classes.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-friends/bp-friends-classes.php  2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-friends/bp-friends-classes.php     2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -364,9 +364,27 @@
</span><span class="cx">  public static function get_bulk_last_active( $user_ids ) {
</span><span class="cx">          global $wpdb;
</span><span class="cx"> 
</span><del>-               $user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
</del><ins>+                $last_activities = BP_Core_User::get_last_activity( $user_ids );
</ins><span class="cx"> 
</span><del>-               return $wpdb->get_results( $wpdb->prepare( "SELECT meta_value as last_activity, user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} ) ORDER BY meta_value DESC", bp_get_user_meta_key( 'last_activity' ) ) );
</del><ins>+                // Sort and structure as expected in legacy function
+               usort( $last_activities, create_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;
</ins><span class="cx">   }
</span><span class="cx"> 
</span><span class="cx">  /**
</span></span></pre></div>
<a id="trunkbploaderphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-loader.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-loader.php      2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-loader.php 2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -304,7 +304,7 @@
</span><span class="cx">          /** Versions **************************************************/
</span><span class="cx"> 
</span><span class="cx">          $this->version    = '2.0-alpha-7752';
</span><del>-               $this->db_version = 7731;
</del><ins>+                $this->db_version = 7859;
</ins><span class="cx"> 
</span><span class="cx">          /** Loading ***************************************************/
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkbpmembersbpmembersfunctionsphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-members/bp-members-functions.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-members/bp-members-functions.php        2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-members/bp-members-functions.php   2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -510,6 +510,8 @@
</span><span class="cx">  global $wpdb;
</span><span class="cx"> 
</span><span class="cx">  if ( !$count = get_transient( 'bp_active_member_count' ) ) {
</span><ins>+               $bp = buddypress();
+
</ins><span class="cx">           // Avoid a costly join by splitting the lookup
</span><span class="cx">          if ( is_multisite() ) {
</span><span class="cx">                  $sql = "SELECT ID FROM {$wpdb->users} WHERE (user_status != 0 OR deleted != 0 OR user_status != 0)";
</span><span class="lines">@@ -519,7 +521,7 @@
</span><span class="cx"> 
</span><span class="cx">          $exclude_users     = $wpdb->get_col( $sql );
</span><span class="cx">          $exclude_users_sql = !empty( $exclude_users ) ? "AND user_id NOT IN (" . implode( ',', wp_parse_id_list( $exclude_users ) ) . ")" : '';
</span><del>-               $count             = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(user_id) FROM {$wpdb->usermeta} WHERE meta_key = %s {$exclude_users_sql}", bp_get_user_meta_key( 'last_activity' ) ) );
</del><ins>+                $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 ) );
</ins><span class="cx"> 
</span><span class="cx">          set_transient( 'bp_active_member_count', $count );
</span><span class="cx">  }
</span><span class="lines">@@ -817,12 +819,13 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><del>- * Update a user's last activity
</del><ins>+ * Update a user's last activity.
</ins><span class="cx">  *
</span><del>- * @since BuddyPress (1.9)
- * @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
</del><ins>+ * @since BuddyPress (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.
</ins><span class="cx">  */
</span><span class="cx"> function bp_update_user_last_activity( $user_id = 0, $time = '' ) {
</span><span class="cx">  // Fall back on current user
</span><span class="lines">@@ -840,24 +843,89 @@
</span><span class="cx">          $time = bp_core_current_time();
</span><span class="cx">  }
</span><span class="cx"> 
</span><del>-       return bp_update_user_meta( $user_id, 'last_activity', $time );
</del><ins>+        // 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, 4 );
+       update_user_meta( $user_id, 'last_activity', $time );
+       add_filter( 'update_user_metadata', '_bp_update_user_meta_last_activity_warning', 10, 4 );
+
+       return BP_Core_User::update_last_activity( $user_id, $time );
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span><del>- * Get the last activity for a given user
</del><ins>+ * Backward compatibility for 'last_activity' usermeta fetching.
</ins><span class="cx">  *
</span><del>- * @param int $user_id The ID of the user
</del><ins>+ * 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 BuddyPress (2.0.0)
+ *
+ * @access private For internal use only.
+ *
+ * @param null $retval
+ * @param int $object_id ID of the user.
+ * @param string $meta_key Meta key being fetched.
+ */
+function _bp_get_user_meta_last_activity_warning( $retval, $object_id, $meta_key ) {
+       static $warned;
+
+       if ( 'last_activity' === $meta_key ) {
+               // Don't send the warning more than once per pageload
+               if ( empty( $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 = 1;
+               }
+
+               return bp_get_user_last_activity( $object_id );
+       }
+
+       return $retval;
+}
+add_filter( 'get_user_metadata', '_bp_get_user_meta_last_activity_warning', 10, 3 );
+
+/**
+ * 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 BuddyPress (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.
+ *
+ * @param int $user_id The ID of the user.
</ins><span class="cx">  * @return string Time of last activity, in 'Y-m-d H:i:s' format, or an empty
</span><del>- *   string if none is found
</del><ins>+ *         string if none is found.
</ins><span class="cx">  */
</span><span class="cx"> function bp_get_user_last_activity( $user_id = 0 ) {
</span><del>-       // Fall back on current user
-       if ( empty( $user_id ) ) {
-               $user_id = bp_loggedin_user_id();
</del><ins>+        $activity = '';
+
+       $last_activity = BP_Core_User::get_last_activity( $user_id );
+       if ( ! empty( $last_activity[ $user_id ] ) ) {
+               $activity = $last_activity[ $user_id ]['date_recorded'];
</ins><span class="cx">   }
</span><span class="cx"> 
</span><del>-       $activity = bp_get_user_meta( $user_id, 'last_activity', true );
-
</del><span class="cx">   return apply_filters( 'bp_get_user_last_activity', $activity, $user_id );
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="lines">@@ -996,8 +1064,8 @@
</span><span class="cx">  */
</span><span class="cx"> function bp_core_remove_data( $user_id ) {
</span><span class="cx"> 
</span><del>-       // Remove usermeta
-       bp_delete_user_meta( $user_id, 'last_activity' );
</del><ins>+        // Remove last_activity data
+       BP_Core_User::delete_last_activity( $user_id );
</ins><span class="cx"> 
</span><span class="cx">  // Flush the cache to remove the user from all cached objects
</span><span class="cx">  wp_cache_flush();
</span></span></pre></div>
<a id="trunkbpmembersbpmembersloaderphp"></a>
<div class="modfile"><h4>Modified: trunk/bp-members/bp-members-loader.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/bp-members/bp-members-loader.php   2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/bp-members/bp-members-loader.php      2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -72,6 +72,9 @@
</span><span class="cx">                  'slug'          => BP_MEMBERS_SLUG,
</span><span class="cx">                  'root_slug'     => isset( $bp->pages->members->slug ) ? $bp->pages->members->slug : BP_MEMBERS_SLUG,
</span><span class="cx">                  'has_directory' => true,
</span><ins>+                       'global_tables' => array(
+                               'table_name_last_activity' => bp_core_get_table_prefix() . 'bp_activity',
+                       ),
</ins><span class="cx">                   'search_string' => __( 'Search Members...', 'buddypress' ),
</span><span class="cx">          ) );
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunktestsincludestestcasephp"></a>
<div class="modfile"><h4>Modified: trunk/tests/includes/testcase.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/tests/includes/testcase.php        2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/tests/includes/testcase.php   2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -218,7 +218,7 @@
</span><span class="cx">  function create_user( $args = array() ) {
</span><span class="cx">          $r = wp_parse_args( $args, array(
</span><span class="cx">                  'role' => 'subscriber',
</span><del>-                       'last_activity' => bp_core_current_time() - 60*60*24*365,
</del><ins>+                        'last_activity' => date( 'Y-m-d h:i:s', strtotime( bp_core_current_time() ) - 60*60*24*365 ),
</ins><span class="cx">           ) );
</span><span class="cx"> 
</span><span class="cx">          $last_activity = $r['last_activity'];
</span></span></pre></div>
<a id="trunkteststestcasescoreclassbpcoreuserphp"></a>
<div class="modfile"><h4>Modified: trunk/tests/testcases/core/class-bp-core-user.php (7859 => 7860)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/tests/testcases/core/class-bp-core-user.php        2014-02-12 17:33:41 UTC (rev 7859)
+++ trunk/tests/testcases/core/class-bp-core-user.php   2014-02-12 21:27:20 UTC (rev 7860)
</span><span class="lines">@@ -2,6 +2,7 @@
</span><span class="cx"> 
</span><span class="cx"> /**
</span><span class="cx">  * @group core
</span><ins>+ * @group BP_Core_User
</ins><span class="cx">  */
</span><span class="cx"> class BP_Tests_BP_Core_User_TestCases extends BP_UnitTestCase {
</span><span class="cx">  protected $old_current_user = 0;
</span><span class="lines">@@ -119,6 +120,55 @@
</span><span class="cx">          $this->assertEquals( array( $u1, $u3 ), $user_ids );
</span><span class="cx">  }
</span><span class="cx"> 
</span><ins>+       /**
+        * @group last_activity
+        */
+       public function test_get_last_activity() {
+               $u = $this->create_user();
+               $time = bp_core_current_time();
</ins><span class="cx"> 
</span><ins>+               BP_Core_User::update_last_activity( $u, $time );
</ins><span class="cx"> 
</span><ins>+               $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
+        */
+       public function test_update_last_activity() {
+               $u = $this->create_user();
+               $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 = $this->create_user();
+               $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 );
+       }
</ins><span class="cx"> }
</span></span></pre>
</div>
</div>

</body>
</html>