diff --git src/includes/admin/tools.php src/includes/admin/tools.php
index 729dc81..7457717 100644
--- src/includes/admin/tools.php
+++ src/includes/admin/tools.php
@@ -1026,7 +1026,8 @@ function bbp_admin_repair_topic_voice_count() {
 	$statement = __( 'Counting the number of voices in each topic&hellip; %s', 'bbpress' );
 	$result    = __( 'Failed!', 'bbpress' );
 
-	$sql_delete = "DELETE FROM `{$bbp_db->postmeta}` WHERE `meta_key` = '_bbp_voice_count';";
+	// Delete our old counts and ids.
+	$sql_delete = "DELETE FROM `{$bbp_db->postmeta}` WHERE `meta_key` IN ( '_bbp_voice_count', '_bbp_voice_ids' );";
 	if ( is_wp_error( $bbp_db->query( $sql_delete ) ) ) {
 		return array( 1, sprintf( $statement, $result ) );
 	}
@@ -1037,21 +1038,50 @@ function bbp_admin_repair_topic_voice_count() {
 	$pps = bbp_get_public_status_id();
 	$cps = bbp_get_closed_status_id();
 
-	$sql = "INSERT INTO `{$bbp_db->postmeta}` (`post_id`, `meta_key`, `meta_value`) (
-			SELECT `postmeta`.`meta_value`, '_bbp_voice_count', COUNT(DISTINCT `post_author`) as `meta_value`
-				FROM `{$bbp_db->posts}` AS `posts`
-				LEFT JOIN `{$bbp_db->postmeta}` AS `postmeta`
-					ON `posts`.`ID` = `postmeta`.`post_id`
-					AND `postmeta`.`meta_key` = '_bbp_topic_id'
-				WHERE `posts`.`post_type` IN ( '{$tpt}', '{$rpt}' )
-					AND `posts`.`post_status` IN ( '{$pps}', '{$cps}' )
-					AND `posts`.`post_author` != '0'
-				GROUP BY `postmeta`.`meta_value`);";
+	$sql = "SELECT `postmeta`.`meta_value` AS `id`, COUNT(DISTINCT `post_author`) AS `count`, GROUP_CONCAT( `post_author` ) AS `voices`
+			FROM `{$bbp_db->posts}` AS `posts`
+			LEFT JOIN `{$bbp_db->postmeta}` AS `postmeta`
+				ON `posts`.`ID` = `postmeta`.`post_id`
+				AND `postmeta`.`meta_key` = '_bbp_topic_id'
+			WHERE ( `posts`.`post_type` = '{$tpt}' AND `posts`.`post_status` IN ( '{$pps}', '{$cps}' ) )
+				OR ( `posts`.`post_type` = '{$rpt}' AND `posts`.`post_status` = '{$pps}' AND `posts`.`post_author` != '0' )
+			GROUP BY `postmeta`.`meta_value`;";
 
-	if ( is_wp_error( $bbp_db->query( $sql ) ) ) {
+	// Get our counts and voice ids.
+	$results = $bbp_db->get_results( $sql );
+
+	if ( is_wp_error( $results ) ) {
 		return array( 2, sprintf( $statement, $result ) );
 	}
 
+	// Set up our MySQL VALUES groups.
+	$values = array();
+	foreach ( $results as $key => $result ) {
+
+		// If we don't have a valid post id, bail.
+		if ( empty( $result->id ) ) {
+			// Unset results item to manage memory.
+			unset( $results[ $key ] );
+			continue;
+		}
+
+		$values[] = "( {$result->id}, '_bbp_voice_count', '{$result->count}' )";
+		$values[] = "( {$result->id}, '_bbp_voice_ids', '{$result->voices}' )";
+
+		// Unset results item to manage memory.
+		unset( $results[ $key ] );
+	}
+
+	// Insert our meta roles.
+	$sql = "INSERT INTO `{$bbp_db->postmeta}` (`post_id`, `meta_key`, `meta_value`) VALUES " . implode( ',', $values ) . ";";
+
+	// Unset values array to manage memory. Probably not needed, but can't hurt.
+	unset( $values );
+
+	if ( is_wp_error( $bbp_db->query( $sql ) ) ) {
+		return array( 3, sprintf( $statement, $result ) );
+	}
+
 	return array( 0, sprintf( $statement, __( 'Complete!', 'bbpress' ) ) );
 }
 
diff --git src/includes/core/actions.php src/includes/core/actions.php
index c9efadb..29bc88a 100644
--- src/includes/core/actions.php
+++ src/includes/core/actions.php
@@ -319,6 +319,20 @@ add_action( 'bbp_delete_topic',  'bbp_delete_topic_replies'  );
 add_action( 'bbp_spam_topic',    'bbp_spam_topic_replies'    );
 add_action( 'bbp_unspam_topic',  'bbp_unspam_topic_replies'  );
 
+// Voice counts.
+add_action( 'bbp_new_reply',        'bbp_maybe_increase_topic_voice_count' );
+add_action( 'bbp_new_topic',        'bbp_maybe_increase_topic_voice_count' );
+add_action( 'bbp_approved_reply',   'bbp_maybe_increase_topic_voice_count' );
+add_action( 'bbp_unspammed_reply',  'bbp_maybe_increase_topic_voice_count' );
+add_action( 'bbp_untrashed_reply',  'bbp_maybe_increase_topic_voice_count' );
+add_action( 'bbp_unapproved_reply', 'bbp_maybe_decrease_topic_voice_count' );
+add_action( 'bbp_spammed_reply',    'bbp_maybe_decrease_topic_voice_count' );
+add_action( 'bbp_trashed_reply',    'bbp_maybe_decrease_topic_voice_count' );
+
+// Insert topic/reply voice counts.
+add_action( 'bbp_insert_topic', 'bbp_maybe_increase_topic_voice_count'        );
+add_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count', 10, 2 );
+
 // User status
 // @todo make these sub-actions
 add_action( 'make_ham_user',  'bbp_make_ham_user'  );
diff --git src/includes/replies/functions.php src/includes/replies/functions.php
index bb66f52..31e8024 100644
--- src/includes/replies/functions.php
+++ src/includes/replies/functions.php
@@ -1018,13 +1018,11 @@ function bbp_update_reply_walker( $reply_id, $last_active_time = '', $forum_id =
 				// See https://bbpress.trac.wordpress.org/ticket/2838
 				bbp_update_topic_last_active_time( $ancestor, $topic_last_active_time );
 
-				// Counts
-				bbp_update_topic_voice_count( $ancestor );
-
 				// Only update reply count if we're deleting a reply, or in the dashboard.
 				if ( in_array( current_filter(), array( 'bbp_deleted_reply', 'save_post' ), true ) ) {
 					bbp_update_topic_reply_count(        $ancestor );
 					bbp_update_topic_reply_count_hidden( $ancestor );
+					bbp_update_topic_voice_count(        $ancestor );
 				}
 
 			// Forum meta relating to most recent topic
diff --git src/includes/topics/functions.php src/includes/topics/functions.php
index 1f5089e..97e82ce 100644
--- src/includes/topics/functions.php
+++ src/includes/topics/functions.php
@@ -2606,6 +2606,176 @@ function bbp_insert_topic_update_counts( $topic_id = 0, $forum_id = 0 ) {
 	}
 }
 
+/**
+ * Maybe bump the topic voice count.
+ *
+ * @since 2.6.0 bbPress (rXXXX)
+ *
+ * @param int    $topic_id The reply id.
+ * @param int    $reply_id The topic id.
+ * @param string $action   Expected `increase` or `decrease`.
+ *
+ * @uses bbp_get_topic_id() To validate the topic id.
+ * @uses bbp_get_reply_id() To validate the reply id.
+ * @uses bbp_get_reply_topic_id() To get the reply's topic id.
+ * @uses get_post_field() To get `the post_author` field.
+ * @uses bbp_is_reply_published() To determine if the reply is published.
+ * @uses bbp_is_topic_published() To determine if the topic is published.
+ * @uses bbp_get_topic_voice_ids() To get the topic voice ids array.
+ * @uses update_post_meta() To update our voice count/ids meta values.
+ * @uses apply_filters() To update our voice count/ids meta values.
+ *
+ * @return bool|int The topic voice count. False on failure.
+ */
+function bbp_maybe_bump_topic_voice_count( $topic_id = 0, $reply_id = 0, $action = '' ) {
+
+	// Valid our topic/reply ids.
+	$topic_id = bbp_get_topic_id( $topic_id );
+	$reply_id = bbp_get_reply_id( $reply_id );
+
+	// Bail early if we don't have valid ids.
+	if ( empty( $topic_id ) && empty( $reply_id ) ) {
+		return false;
+	}
+
+	// Bail if we don't have a valid action.
+	if ( ! in_array( $action, array( 'increase', 'decrease' ), true ) ) {
+		return false;
+	}
+
+	// Make sure we have a topic id.
+	if ( empty( $topic_id ) ) {
+		$topic_id = bbp_get_reply_topic_id( $reply_id );
+	}
+
+	// Set some defaults.
+	$published = $anonymous = false;
+	$author    = 0;
+
+	// Possibly update our author and published variables.
+	if ( ! empty( $reply_id ) ) {
+		$author    = bbp_get_reply_author_id( $reply_id );
+		$anonymous = empty( $author );
+
+		// Don't waste a potential call to the db, if it won't be counted anyway.
+		if ( ! $anonymous ) {
+			$published = bbp_is_reply_published( $reply_id );
+		}
+	} else {
+		$author    = bbp_get_topic_author_id( $topic_id );
+		$published = bbp_is_topic_published( $topic_id );
+	}
+
+	// Get the topic voice ids.
+	$voice_ids = bbp_get_topic_voice_ids( $topic_id );
+
+	// If we have a valid author and we're published, update the voice ids array.
+	if ( $published && 'increase' === $action && ! $anonymous ) {
+		$voice_ids[] = $author;
+	} elseif ( ! $published && 'decrease' === $action && ! $anonymous ) {
+		unset( $voice_ids[ array_search( $author, $voice_ids, true ) ] );
+	}
+
+	// Count the unique voices.
+	$voice_count = count( array_unique( $voice_ids ) );
+
+	// Concentate our voice ids back to a string for the db.
+	$voice_ids = implode( ',', $voice_ids );
+
+	// Only update voice ids and count for a topic if it's not an anonymous reply.
+	if ( ! $anonymous ) {
+		update_post_meta( $topic_id, '_bbp_voice_ids', $voice_ids );
+		update_post_meta( $topic_id, '_bbp_voice_count', $voice_count );
+	}
+
+	/**
+	 * Filters the return of a voice count bump for a topic.
+	 *
+	 * @since 2.6.0 bbPress (rXXXX)
+	 *
+	 * @param int    $voice_count The topic voice count.
+	 * @param array  $voice_ids   Array of voice ids.
+	 * @param int    $topic_id    The topic id.
+	 * @param int    $reply_id    The reply id.
+	 * @param string $action      The action being taken (`increase` or `decrease`).
+	 */
+	return (int) apply_filters( 'bbp_maybe_bump_topic_voice_count', $voice_count, $voice_ids, $topic_id, $reply_id, $action );
+}
+
+/**
+ * Maybe increase the topic voice count.
+ *
+ * @since 2.6.0 bbPress (rXXXX)
+ *
+ * @param int $topic_id The topic id.
+ * @param int $reply_id The reply id.
+ *
+ * @uses bbp_is_reply() To determine if passed topic id is actually a reply.
+ * @uses bbp_get_reply_id() To validate the reply id.
+ * @uses bbp_get_reply_topic_id() To get the reply's topic id.
+ * @uses bbp_is_topic() To determine if passed topic id is actually a topic.
+ * @uses bbp_get_topic_id() To validate the topic id.
+ * @uses bbp_maybe_bump_topic_voice_count() To maybe increase the topic voice count.
+ *
+ * @return bool|int The topic voice count. False on failure.
+ */
+function bbp_maybe_increase_topic_voice_count( $topic_id = 0, $reply_id = 0 ) {
+
+	// If it's a reply, then get the topic id, and validate the reply id.
+	if ( bbp_is_reply( $topic_id ) ) {
+		$reply_id = bbp_get_reply_id( $topic_id );
+		$topic_id = bbp_get_reply_topic_id( $reply_id );
+
+	// If it's a topic, then validate the passed ids.
+	} elseif ( bbp_is_topic( $topic_id ) ) {
+		$reply_id = bbp_get_reply_id( $reply_id );
+		$topic_id = bbp_get_topic_id( $topic_id );
+
+	// Bail if the passed id isn't a topic or reply.
+	} else {
+		return false;
+	}
+
+	return bbp_maybe_bump_topic_voice_count( $topic_id, $reply_id, 'increase' );
+}
+
+/**
+ * Maybe decrease the topic voice count.
+ *
+ * @since 2.6.0 bbPress (rXXXX)
+ *
+ * @param int $topic_id The reply id.
+ * @param int $reply_id The topic id.
+ *
+ * @uses bbp_is_reply() To determine if passed topic id is actually a reply.
+ * @uses bbp_get_reply_id() To validate the reply id.
+ * @uses bbp_get_reply_topic_id() To get the reply's topic id.
+ * @uses bbp_is_topic() To determine if passed topic id is actually a topic.
+ * @uses bbp_get_topic_id() To validate the topic id.
+ * @uses bbp_maybe_bump_topic_voice_count() To maybe decrease the topic voice count.
+ *
+ * @return bool|int The topic voice count. False on failure.
+ */
+function bbp_maybe_decrease_topic_voice_count( $topic_id = 0, $reply_id = 0 ) {
+
+	// If it's a reply, then get the topic id, and validate the reply id.
+	if ( bbp_is_reply( $topic_id ) ) {
+		$reply_id = bbp_get_reply_id( $topic_id );
+		$topic_id = bbp_get_reply_topic_id( $reply_id );
+
+	// If it's a topic, then validate the passed ids.
+	} elseif ( bbp_is_topic( $topic_id ) ) {
+		$reply_id = bbp_get_reply_id( $reply_id );
+		$topic_id = bbp_get_topic_id( $topic_id );
+
+	// Bail if the passed id isn't a topic or reply.
+	} else {
+		return false;
+	}
+
+	return bbp_maybe_bump_topic_voice_count( $topic_id, $reply_id, 'decrease' );
+}
+
 /** Topic Updaters ************************************************************/
 
 /**
@@ -2913,13 +3083,16 @@ function bbp_update_topic_voice_count( $topic_id = 0 ) {
 
 	// Query the DB to get voices in this topic
 	$bbp_db = bbp_db();
-	$query  = $bbp_db->prepare( "SELECT COUNT( DISTINCT post_author ) FROM {$bbp_db->posts} WHERE ( post_parent = %d AND post_status = '%s' AND post_type = '%s' ) OR ( ID = %d AND post_type = '%s' );", $topic_id, bbp_get_public_status_id(), bbp_get_reply_post_type(), $topic_id, bbp_get_topic_post_type() );
-	$voices = (int) $bbp_db->get_var( $query );
+	$query  = $bbp_db->prepare( "SELECT post_author FROM {$bbp_db->posts} WHERE ( post_parent = %d AND post_status = '%s' AND post_type = '%s' AND post_author != 0 ) OR ( ID = %d AND post_type = '%s' );", $topic_id, bbp_get_public_status_id(), bbp_get_reply_post_type(), $topic_id, bbp_get_topic_post_type() );
+	$voice_ids = $bbp_db->get_col( $query );
+	$voice_ids = array_map( 'absint', (array) $voice_ids );
+	$count     = count( array_unique( $voice_ids ) );
 
-	// Update the voice count for this topic id
-	update_post_meta( $topic_id, '_bbp_voice_count', $voices );
+	// Update the voice ids and count for this topic id
+	update_post_meta( $topic_id, '_bbp_published_voice_ids', implode( ',', $voice_ids ) );
+	update_post_meta( $topic_id, '_bbp_voice_count', $count );
 
-	return (int) apply_filters( 'bbp_update_topic_voice_count', $voices, $topic_id );
+	return (int) apply_filters( 'bbp_update_topic_voice_count', $count, $topic_id );
 }
 
 /**
diff --git src/includes/topics/template.php src/includes/topics/template.php
index 18270c9..1c516ef 100644
--- src/includes/topics/template.php
+++ src/includes/topics/template.php
@@ -2354,6 +2354,70 @@ function bbp_topic_voice_count( $topic_id = 0, $integer = false ) {
 	}
 
 /**
+ * Return an array of topic voice ids.
+ *
+ * All `post_author` ids of public replies, along with public and closed topics
+ * are included in this array. Authors of anonymous topics are counted as one
+ * voice. Anonymous replies are not included. Voice ids are stored in the
+ * post_meta table as a comma-separated list, and converted to an array before
+ * being returned.
+ *
+ * @since 2.6.0 bbPress (rXXXX)
+ *
+ * @param int $topic_id The topic id. Required.
+ *
+ * @uses bbp_get_topic_id() To validate the topic id.
+ * @uses get_post_meta() To get the `_bbp_voice_ids` meta value.
+ * @uses bbp_db() To get the wpdb object.
+ * @uses bbp_get_public_status_id() To get the public status id.
+ * @uses bbp_get_reply_post_type() To get the reply post type.
+ * @uses bbp_get_topic_post_type() To get the topic post type.
+ *
+ * @return bool|array An array of voice ids. False on failure.
+ */
+function bbp_get_topic_voice_ids( $topic_id = 0 ) {
+
+	// Validate the topic id.
+	$topic_id = bbp_get_topic_id( $topic_id );
+
+	// Bail if the topic id isn't valid.
+	if ( empty( $topic_id ) ) {
+		return false;
+	}
+
+	// Check the post meta table first.
+	$voice_ids = get_post_meta( $topic_id, '_bbp_voice_ids', true );
+
+	// If no voice ids, get them from the db.
+	if ( false === $voice_ids ) {
+		// Query the DB to get voices in this topic.
+		$bbp_db = bbp_db();
+		$query  = $bbp_db->prepare( "SELECT post_author FROM {$bbp_db->posts} WHERE ( post_parent = %d AND post_status = '%s' AND post_type = '%s' AND post_author != 0 ) OR ( ID = %d AND post_type = '%s' );", $topic_id, bbp_get_public_status_id(), bbp_get_reply_post_type(), $topic_id, bbp_get_topic_post_type() );
+		$voice_ids = $bbp_db->get_col( $query );
+	}
+
+	// Make sure we have an array.
+	if ( ! is_array( $voice_ids ) ) {
+		$voice_ids = explode( ',', $voice_ids );
+	}
+
+	// Clean up the voice ids array. We don't want empty strings converted to an
+	// integer of 0, so we need to filter these out.
+	$voice_ids = array_filter( array_map( 'trim', $voice_ids ), 'strlen' );
+	$voice_ids = array_map( 'absint', $voice_ids );
+
+	/**
+	 * Filters the voice ids array for a topic.
+	 *
+	 * @since 2.6.0 bbPress (rXXXX)
+	 *
+	 * @param array $voice_ids Array of voice ids.
+	 * @param int   $topic_id  The topic id.
+	 */
+	return (array) apply_filters( 'bbp_get_topic_voice_ids', $voice_ids, $topic_id );
+}
+
+/**
  * Output a the tags of a topic
  *
  * @since 2.0.0 bbPress (r2688)
diff --git tests/phpunit/testcases/topics/functions/counts.php tests/phpunit/testcases/topics/functions/counts.php
index 3524ae7..0e610f7 100644
--- tests/phpunit/testcases/topics/functions/counts.php
+++ tests/phpunit/testcases/topics/functions/counts.php
@@ -557,6 +557,9 @@ class BBP_Tests_Topics_Functions_Counts extends BBP_UnitTestCase {
 		$count = bbp_update_topic_voice_count( $t );
 		$this->assertSame( 2, $count );
 
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0] ), $voices );
+
 		$r = $this->factory->reply->create( array(
 			'post_author' => $u[1],
 			'post_parent' => $t,
@@ -565,6 +568,9 @@ class BBP_Tests_Topics_Functions_Counts extends BBP_UnitTestCase {
 		bbp_update_topic_voice_count( $t );
 		$count = bbp_get_topic_voice_count( $t );
 		$this->assertSame( '3', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1] ), $voices );
 	}
 
 	/**
@@ -577,4 +583,245 @@ class BBP_Tests_Topics_Functions_Counts extends BBP_UnitTestCase {
 			'This test has not been implemented yet.'
 		);
 	}
+
+	/**
+	 * @covers ::bbp_maybe_bump_topic_voice_count
+	 */
+	public function test_bbp_maybe_bump_topic_voice_count() {
+		$u = $this->factory->user->create_many( 2 );
+		$t = $this->factory->topic->create();
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '1', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id() ), $voices );
+
+		remove_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count' );
+
+		$r1 = $this->factory->reply->create( array(
+			'post_author' => $u[0],
+			'post_parent' => $t,
+		) );
+
+		bbp_maybe_bump_topic_voice_count( $t, $r1, 'increase' );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '2', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0] ), $voices );
+
+		$r2 = $this->factory->reply->create( array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+
+		bbp_maybe_bump_topic_voice_count( $t, $r2, 'increase' );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '3', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1] ), $voices );
+
+		$r3 = $this->factory->reply->create( array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+
+		bbp_maybe_bump_topic_voice_count( $t, $r3, 'increase' );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '3', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1], $u[1] ), $voices );
+
+		remove_action( 'bbp_unapproved_reply', 'bbp_maybe_decrease_topic_voice_count' );
+
+		bbp_unapprove_reply( $r1 );
+
+		add_action( 'bbp_unapproved_reply', 'bbp_maybe_decrease_topic_voice_count' );
+
+		bbp_maybe_bump_topic_voice_count( $t, $r1, 'decrease' );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '2', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[1], $u[1] ), $voices );
+
+		add_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count' );
+	}
+
+	/**
+	 * @covers ::bbp_maybe_increase_topic_voice_count
+	 */
+	public function test_bbp_maybe_increase_topic_voice_count() {
+		$u = $this->factory->user->create_many( 2 );
+		$t = $this->factory->topic->create();
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '1', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id() ), $voices );
+
+		remove_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count' );
+
+		$r = $this->factory->reply->create( array(
+			'post_author' => $u[0],
+			'post_parent' => $t,
+		) );
+
+		bbp_maybe_increase_topic_voice_count( $t, $r );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '2', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0] ), $voices );
+
+		$r = $this->factory->reply->create( array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+
+		bbp_maybe_increase_topic_voice_count( $r );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '3', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1] ), $voices );
+
+		add_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count' );
+	}
+
+	/**
+	 * @covers ::bbp_maybe_increase_topic_voice_count
+	 */
+	public function test_bbp_maybe_increase_topic_voice_count_for_bbp_new_topic_or_reply() {
+
+		// We don't want `bbp_insert_(topic|reply)` to jump the gun.
+		remove_action( 'bbp_insert_topic', 'bbp_maybe_increase_topic_voice_count' );
+		remove_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count' );
+
+		$u = $this->factory->user->create_many( 2 );
+		$t = $this->factory->topic->create( array(
+			'post_author' => $u[0],
+		) );
+
+		do_action( 'bbp_new_topic', $t, 0, array(), bbp_get_current_user_id() );
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '1', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( $u[0] ), $voices );
+
+		$r1 = $this->factory->reply->create( array(
+			'post_author' => $u[0],
+			'post_parent' => $t,
+		) );
+
+		do_action( 'bbp_new_reply', $r1, $t, 0, array(), $u[0] );
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '1', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( $u[0], $u[0] ), $voices );
+
+		$r2 = $this->factory->reply->create( array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+
+		do_action( 'bbp_new_reply', $r2, $t, 0, array(), $u[1] );
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '2', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( $u[0], $u[0], $u[1] ), $voices );
+
+		add_action( 'bbp_insert_topic', 'bbp_maybe_increase_topic_voice_count' );
+		add_action( 'bbp_insert_reply', 'bbp_maybe_increase_topic_voice_count' );
+	}
+
+	/**
+	 * @covers ::bbp_maybe_decrease_topic_voice_count
+	 */
+	public function test_bbp_maybe_decrease_topic_voice_count() {
+		$u = $this->factory->user->create_many( 2 );
+		$t = $this->factory->topic->create();
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '1', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id() ), $voices );
+
+		$r1 = $this->factory->reply->create( array(
+			'post_author' => $u[0],
+			'post_parent' => $t,
+		) );
+		$r2 = $this->factory->reply->create( array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+		$r3 = $this->factory->reply->create( array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+
+		remove_action( 'bbp_unapproved_reply', 'bbp_maybe_decrease_topic_voice_count' );
+
+		bbp_unapprove_reply( $r3 );
+
+		add_action( 'bbp_unapproved_reply', 'bbp_maybe_decrease_topic_voice_count' );
+
+		bbp_maybe_decrease_topic_voice_count( $t, $r3 );
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '3', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1] ), $voices );
+
+		bbp_unapprove_reply( $r1 );
+
+		$count = bbp_get_topic_voice_count( $t );
+		$this->assertSame( '2', $count );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[1] ), $voices );
+	}
+
+	/**
+	 * @covers ::bbp_get_topic_voice_ids
+	 */
+	public function test_bbp_get_topic_voice_ids() {
+		$u = $this->factory->user->create_many( 2 );
+		$t = $this->factory->topic->create();
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id() ), $voices );
+
+		$r1 = $this->factory->reply->create( array(
+			'post_author' => $u[0],
+			'post_parent' => $t,
+		) );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0] ), $voices );
+
+		$r2 = $this->factory->reply->create_many( 2, array(
+			'post_author' => $u[1],
+			'post_parent' => $t,
+		) );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1], $u[1] ), $voices );
+
+		bbp_unapprove_reply( $r2[0] );
+
+		$voices = bbp_get_topic_voice_ids( $t );
+		$this->assertEquals( array( bbp_get_current_user_id(), $u[0], $u[1] ), $voices );
+	}
 }
