register_hooks();
}
/**
* Register the WordPress hooks
*/
private function register_hooks() {
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
if ( function_exists( 'classicpress_version' ) ) {
return;
}
add_action( 'admin_enqueue_scripts', array( $this, 'language_data' ) );
add_filter( 'render_block', array( $this, 'render_friends_block_visibility' ), 10, 2 );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_sidebar_blocks' ) );
add_action( 'init', array( $this, 'register_blocks' ) );
}
/**
* Safely get block wrapper attributes, returning a fallback when not in a block context.
*
* @param array $extra_attributes Extra attributes to add.
* @return string The wrapper attributes string.
*/
private function get_wrapper_attributes( $extra_attributes = array() ) {
if ( function_exists( 'get_block_wrapper_attributes' ) && \WP_Block_Supports::$block_to_render ) {
return get_block_wrapper_attributes( $extra_attributes );
}
$attrs = '';
foreach ( $extra_attributes as $key => $value ) {
$attrs .= ' ' . $key . '="' . esc_attr( $value ) . '"';
}
return $attrs;
}
/**
* Register our blocks.
*/
public function register_blocks() {
if ( ! function_exists( 'register_block_type_from_metadata' ) ) {
// Blocks is not active.
return;
}
register_block_type_from_metadata(
FRIENDS_PLUGIN_DIR . '/blocks/friends-list',
array(
'render_callback' => array( $this, 'render_friends_list_block' ),
)
);
register_block_type(
'friends/subscriptions-query',
array(
'render_callback' => array( $this, 'render_subscriptions_query_block' ),
)
);
register_block_type(
'friends/subscription',
array(
'uses_context' => array( 'friends/subscription' ),
'render_callback' => array( $this, 'render_subscription_block' ),
)
);
register_block_type(
'friends/followers',
array(
'render_callback' => array( $this, 'render_followers_block' ),
)
);
register_block_type(
'friends/add-friend-form',
array(
'render_callback' => array( $this, 'render_add_friend_form_block' ),
)
);
$list_supports = array(
'color' => array(
'background' => true,
'text' => true,
'link' => true,
),
'typography' => array(
'fontSize' => true,
'lineHeight' => true,
),
'spacing' => array(
'padding' => true,
'margin' => true,
),
);
$chip_supports = array(
'color' => array(
'background' => true,
'text' => true,
'link' => true,
),
'typography' => array(
'fontSize' => true,
),
'spacing' => array(
'padding' => true,
'margin' => true,
),
);
register_block_type(
'friends/stats',
array(
'render_callback' => array( $this, 'render_stats_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/refresh',
array(
'render_callback' => array( $this, 'render_refresh_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/post-formats',
array(
'render_callback' => array( $this, 'render_post_formats_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/add-subscription',
array(
'render_callback' => array( $this, 'render_add_subscription_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/search',
array(
'render_callback' => array( $this, 'render_search_block' ),
)
);
register_block_type(
'friends/feed-title',
array(
'render_callback' => array( $this, 'render_feed_title_block' ),
'supports' => array(
'color' => array(
'background' => true,
'text' => true,
'link' => true,
),
'typography' => array(
'fontSize' => true,
'lineHeight' => true,
),
),
)
);
register_block_type(
'friends/feed-chips',
array(
'render_callback' => array( $this, 'render_feed_chips_block' ),
'supports' => $chip_supports,
)
);
register_block_type(
'friends/welcome',
array(
'attributes' => array(
'forceWelcome' => array(
'type' => 'boolean',
'default' => false,
),
),
'render_callback' => array( $this, 'render_welcome_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/post-content',
array(
'render_callback' => array( $this, 'render_post_content_block' ),
)
);
register_block_type(
'friends/post-permalink',
array(
'render_callback' => array( $this, 'render_post_permalink_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/post-reblog',
array(
'render_callback' => array( $this, 'render_post_reblog_block' ),
)
);
register_block_type(
'friends/post-boost',
array(
'render_callback' => array( $this, 'render_post_boost_block' ),
)
);
register_block_type(
'friends/post-reactions',
array(
'render_callback' => array( $this, 'render_post_reactions_block' ),
)
);
register_block_type(
'friends/post-comments',
array(
'render_callback' => array( $this, 'render_post_comments_block' ),
)
);
register_block_type(
'friends/author-star',
array(
'render_callback' => array( $this, 'render_author_star_block' ),
)
);
register_block_type(
'friends/author-avatar',
array(
'render_callback' => array( $this, 'render_author_avatar_block' ),
)
);
register_block_type(
'friends/author-name',
array(
'render_callback' => array( $this, 'render_author_name_block' ),
'supports' => array(
'color' => array(
'background' => true,
'text' => true,
'link' => true,
),
'typography' => array(
'fontSize' => true,
'lineHeight' => true,
),
),
)
);
register_block_type(
'friends/author-description',
array(
'render_callback' => array( $this, 'render_author_description_block' ),
'supports' => $list_supports,
)
);
register_block_type(
'friends/author-chips',
array(
'render_callback' => array( $this, 'render_author_chips_block' ),
'supports' => $chip_supports,
)
);
}
/**
* Render the friends/subscriptions-query block.
*
* @param array $attributes Block attributes.
* @param string $content Inner block content.
* @param WP_Block $block Block instance.
* @return string The rendered block HTML.
*/
public function render_subscriptions_query_block( $attributes, $content, $block ) {
$subscriptions = User_Query::all_subscriptions()->get_results();
if ( empty( $subscriptions ) ) {
return '
' . esc_html__( "You're not following anyone yet.", 'friends' ) . '
';
}
$items = '';
foreach ( $subscriptions as $subscription ) {
$context = array_merge(
$block->context,
array(
'friends/subscription' => array(
'ID' => $subscription->ID,
'display_name' => $subscription->display_name,
'user_url' => $subscription->user_url,
'avatar_url' => $subscription->get_avatar_url(),
'page_url' => $subscription->get_local_friends_page_url(),
),
)
);
$inner_content = '';
foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) {
$inner_content .= ( new \WP_Block( $inner_block, $context ) )->render();
}
$items .= '' . $inner_content . ' ';
}
return '';
}
/**
* Render the friends/add-friend-form block.
*
* @return string The rendered block HTML.
*/
public function render_add_friend_form_block() {
ob_start();
Friends::template_loader()->get_template_part( 'frontend/add-friend-form' );
$form = ob_get_clean();
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-add-friend-form' ) ) . '>' . $form . '
';
}
/**
* Render the friends/subscription block.
*
* @param array $attributes Block attributes.
* @param string $content Inner block content.
* @param WP_Block $block Block instance.
* @return string The rendered block HTML.
*/
public function render_subscription_block( $attributes, $content, $block ) {
if ( empty( $block->context['friends/subscription'] ) ) {
return '';
}
$subscription = $block->context['friends/subscription'];
$out = '';
return $out;
}
/**
* Render the friends/followers block.
*
* @return string The rendered block HTML.
*/
public function render_followers_block() {
global $friends_args;
if ( ! class_exists( '\ActivityPub\Collection\Followers' ) ) {
return '' . esc_html__( 'The follower list is currently dependent on the ActivityPub plugin.', 'friends' ) . '
';
}
$user_id = isset( $friends_args['user_id'] ) ? $friends_args['user_id'] : get_current_user_id();
$blog_followers = class_exists( '\ActivityPub\Collection\Actors' ) && \ActivityPub\Collection\Actors::BLOG_USER_ID === $user_id;
$filter = isset( $_GET['filter'] ) ? sanitize_key( $_GET['filter'] ) : 'all'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! in_array( $filter, array( 'all', 'following', 'not-following' ), true ) ) {
$filter = 'all';
}
// Backwards compatibility with ?mutual.
if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$filter = 'following';
}
$sort = isset( $_GET['sort'] ) ? sanitize_key( $_GET['sort'] ) : 'newest'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! in_array( $sort, array( 'newest', 'oldest', 'name' ), true ) ) {
$sort = 'newest';
}
$search_term = isset( $_GET['q'] ) ? trim( wp_unslash( $_GET['q'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$followers_list_page = 'users.php?page=activitypub-followers-list';
if ( $blog_followers ) {
$followers_list_page = 'options-general.php?page=activitypub&tab=followers';
}
$follower_data = \ActivityPub\Collection\Followers::query( $user_id );
$total = $follower_data['total'];
// Apply search filter first so downstream work operates on the smaller set.
if ( '' !== $search_term ) {
$search_term_lc = function_exists( 'mb_strtolower' ) ? mb_strtolower( $search_term ) : strtolower( $search_term );
$follower_data['followers'] = array_values(
array_filter(
$follower_data['followers'],
function ( $f ) use ( $search_term_lc ) {
$haystack = '';
if ( isset( $f->post_title ) ) {
$haystack .= $f->post_title . ' ';
}
if ( isset( $f->guid ) ) {
$haystack .= $f->guid . ' ';
}
if ( isset( $f->post_content ) ) {
$haystack .= wp_strip_all_tags( $f->post_content );
}
$haystack = function_exists( 'mb_strtolower' ) ? mb_strtolower( $haystack ) : strtolower( $haystack );
return false !== strpos( $haystack, $search_term_lc );
}
)
);
}
// Classify followers only when a filter is active.
if ( 'all' !== $filter ) {
$following_ids = array();
$not_following_ids = array();
foreach ( $follower_data['followers'] as $follower ) {
$url = $follower->guid;
$is_following = false;
if ( $url ) {
$is_following = User_Feed::get_by_url( $url );
if ( ! $is_following || is_wp_error( $is_following ) ) {
$is_following = User_Feed::get_by_url( str_replace( '@', 'users/', $url ) );
}
}
if ( $is_following && ! is_wp_error( $is_following ) ) {
$following_ids[] = $follower->ID;
} else {
$not_following_ids[] = $follower->ID;
}
}
if ( 'following' === $filter ) {
$filtered_followers = array_filter(
$follower_data['followers'],
function ( $f ) use ( $following_ids ) {
return in_array( $f->ID, $following_ids, true );
}
);
} else {
$filtered_followers = array_filter(
$follower_data['followers'],
function ( $f ) use ( $not_following_ids ) {
return in_array( $f->ID, $not_following_ids, true );
}
);
}
} else {
$filtered_followers = $follower_data['followers'];
}
// Sort.
$filtered_followers = array_values( $filtered_followers );
if ( 'oldest' === $sort ) {
usort(
$filtered_followers,
function ( $a, $b ) {
return $a->ID - $b->ID;
}
);
} elseif ( 'name' === $sort ) {
usort(
$filtered_followers,
function ( $a, $b ) {
return strnatcasecmp( $a->post_title, $b->post_title );
}
);
}
// Paginate.
$filtered_total = count( $filtered_followers );
$followers_per_page = 20;
$current_page = isset( $_GET['fpage'] ) ? max( 1, absint( $_GET['fpage'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page_followers = array_slice( $filtered_followers, ( $current_page - 1 ) * $followers_per_page, $followers_per_page );
$display_followers = array();
foreach ( $page_followers as $follower ) {
if ( $follower instanceof \WP_Post ) {
$follower = \Activitypub\Collection\Remote_Actors::get_actor( $follower );
if ( is_wp_error( $follower ) ) {
continue;
}
}
$data = $follower->to_array();
$data['url'] = \ActivityPub\object_to_uri( $data['url'] );
$data['server'] = wp_parse_url( $data['url'], PHP_URL_HOST );
$data['css_class'] = '';
$following = User_Feed::get_by_url( $data['url'] );
if ( ! $following || is_wp_error( $following ) ) {
$following = User_Feed::get_by_url( str_replace( '@', 'users/', $data['url'] ) );
}
if ( $following && ! is_wp_error( $following ) ) {
$data['friend_user'] = $following->get_friend_user();
$data['action_url'] = $following->get_friend_user()->get_local_friends_page_url();
$data['url'] = $following->get_friend_user()->get_local_friends_page_url();
if ( 'all' === $filter ) {
$data['css_class'] = ' already-following';
}
} else {
$data['friend_user'] = false;
$data['action_url'] = add_query_arg( 'url', $data['url'], admin_url( 'admin.php?page=add-friend' ) );
}
$data['remove_action_url'] = add_query_arg( 's', $data['url'], admin_url( $followers_list_page ) );
$display_followers[] = $data;
}
$base_url = strtok( $_SERVER['REQUEST_URI'], '?' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$link_args = array();
if ( 'all' !== $filter ) {
$link_args['filter'] = $filter;
}
if ( 'newest' !== $sort ) {
$link_args['sort'] = $sort;
}
if ( '' !== $search_term ) {
$link_args['q'] = $search_term;
}
ob_start();
?>
__( 'All', 'friends' ),
'following' => __( 'Following', 'friends' ),
'not-following' => __( 'Not Following', 'friends' ),
);
$filter_links = array();
foreach ( $filters as $filter_key => $filter_label ) {
$url = $base_url;
$f_args = $link_args;
if ( 'all' !== $filter_key ) {
$f_args['filter'] = $filter_key;
} else {
unset( $f_args['filter'] );
}
if ( $f_args ) {
$url = add_query_arg( $f_args, $url );
}
if ( $filter === $filter_key ) {
$filter_links[] = '' . esc_html( $filter_label ) . ' ';
} else {
$filter_links[] = '' . esc_html( $filter_label ) . ' ';
}
}
echo wp_kses(
implode( ' | ', $filter_links ),
array(
'strong' => array(),
'a' => array( 'href' => array() ),
)
);
?>
__( 'Newest', 'friends' ),
'oldest' => __( 'Oldest', 'friends' ),
'name' => __( 'Name', 'friends' ),
);
$sort_links = array();
foreach ( $sorts as $sort_key => $sort_label ) {
$url = $base_url;
$s_args = $link_args;
if ( 'newest' !== $sort_key ) {
$s_args['sort'] = $sort_key;
} else {
unset( $s_args['sort'] );
}
if ( $s_args ) {
$url = add_query_arg( $s_args, $url );
}
if ( $sort === $sort_key ) {
$sort_links[] = '' . esc_html( $sort_label ) . ' ';
} else {
$sort_links[] = '' . esc_html( $sort_label ) . ' ';
}
}
echo wp_kses(
implode( ' | ', $sort_links ),
array(
'strong' => array(),
'a' => array( 'href' => array() ),
)
);
?>
(@ )
array( 'href' => array() ),
)
);
?>
1 ) {
$pagination_args = array(
'base' => add_query_arg( 'fpage', '%#%' ),
'format' => '',
'current' => $current_page,
'total' => $total_pages,
);
if ( $link_args ) {
$pagination_args['add_args'] = $link_args;
}
echo '';
}
?>
get_total();
$out = 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-stats' ) ) . '>';
if ( class_exists( '\ActivityPub\Collection\Followers' ) && \defined( 'ACTIVITYPUB_ACTOR_MODE' ) ) {
$activitypub_actor_mode = \get_option( 'activitypub_actor_mode', \ACTIVITYPUB_ACTOR_MODE );
if ( \ACTIVITYPUB_ACTOR_MODE === $activitypub_actor_mode || \ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $activitypub_actor_mode ) {
$mutual_count = Feed_Parser_ActivityPub::count_mutual_followers( get_current_user_id() );
$out .= '';
$out .= esc_html(
sprintf(
/* translators: %s: number of mutual friends */
_n( '%s Friend', '%s Friends', $mutual_count, 'friends' ),
$mutual_count
)
);
$out .= ' ';
$follower_count = Feed_Parser_ActivityPub::count_followers( get_current_user_id() );
$out .= '';
$out .= esc_html(
sprintf(
/* translators: %s: number of followers */
_n( '%s Follower', '%s Followers', $follower_count, 'friends' ),
$follower_count
)
);
$out .= ' ';
}
if ( \ACTIVITYPUB_BLOG_MODE === $activitypub_actor_mode || \ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $activitypub_actor_mode ) {
if ( class_exists( '\ActivityPub\Collection\Actors' ) ) {
$blog_follower_count = Feed_Parser_ActivityPub::count_followers( \ActivityPub\Collection\Actors::BLOG_USER_ID );
$out .= '';
$out .= esc_html(
sprintf(
/* translators: %s: number of followers */
_n( '%s Blog Follower', '%s Blog Followers', $blog_follower_count, 'friends' ),
$blog_follower_count
)
);
$out .= ' ';
}
}
}
$out .= '';
$out .= esc_html(
sprintf(
/* translators: %s: number of subscriptions */
_n( '%s Following', '%s Following', $subscriptions_count, 'friends' ),
$subscriptions_count
)
);
$out .= ' ';
$out .= ' ';
return $out;
}
/**
* Render the friends/refresh block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_refresh_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-refresh' ) ) . '>' . esc_html__( 'Refresh', 'friends' ) . '
';
}
/**
* Render the friends/post-formats block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_post_formats_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$out = '';
return $out;
}
/**
* Render the friends/add-subscription block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_add_subscription_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$out = '';
return $out;
}
/**
* Render the friends/search block.
*
* @return string The rendered block HTML.
*/
public function render_search_block() {
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$out = '';
$out .= '
';
return $out;
}
/**
* Render the friends/feed-header block.
*
* @return string The rendered block HTML.
*/
/**
* Render the friends/feed-title block.
*
* @return string The rendered block HTML.
*/
public function render_feed_title_block() {
$friends = Friends::get_instance();
$frontend = $friends->frontend;
// Determine feed title.
$title = __( 'Main Feed', 'friends' );
if ( $frontend->template ) {
$template_titles = array(
'frontend/add-friend' => __( 'Add Friend', 'friends' ),
'frontend/followers' => __( 'Followers', 'friends' ),
'frontend/subscriptions' => __( 'Following', 'friends' ),
);
if ( isset( $template_titles[ $frontend->template ] ) ) {
$title = $template_titles[ $frontend->template ];
}
}
$format_titles = array(
'standard' => _x( 'Post feed', 'Post format', 'friends' ),
'aside' => _x( 'Aside feed', 'Post format', 'friends' ),
'chat' => _x( 'Chat feed', 'Post format', 'friends' ),
'gallery' => _x( 'Gallery feed', 'Post format', 'friends' ),
'link' => _x( 'Link feed', 'Post format', 'friends' ),
'image' => _x( 'Image feed', 'Post format', 'friends' ),
'quote' => _x( 'Quote feed', 'Post format', 'friends' ),
'status' => _x( 'Status feed', 'Post format', 'friends' ),
'video' => _x( 'Video feed', 'Post format', 'friends' ),
'audio' => _x( 'Audio feed', 'Post format', 'friends' ),
);
if ( $frontend->post_format && isset( $format_titles[ $frontend->post_format ] ) ) {
$title = $format_titles[ $frontend->post_format ];
}
if ( isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$title = sprintf(
// translators: %s is a search term.
__( 'Search for "%s"', 'friends' ),
esc_html( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
);
}
// Build display title with reaction/tag context.
$display_title = $title;
if ( $frontend->reaction ) {
$display_title = sprintf(
// translators: %1$s is an emoji reaction, %2$s is a type of feed.
__( 'My %1$s reactions on %2$s', 'friends' ),
$frontend->reaction,
$title
);
} elseif ( $frontend->tag ) {
$display_title = sprintf(
// translators: %1$s is a hash tag, %2$s is a type of feed.
_x( '#%1$s on %2$s', '#tag on feed', 'friends' ),
$frontend->tag,
$title
);
}
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-feed-title' ) ) . '>' . esc_html( $display_title ) . ' ';
}
/**
* Render the friends/feed-chips block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_feed_chips_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$friends = Friends::get_instance();
$data = $friends->get_main_header_data();
$hidden_post_count = 0;
if ( isset( $data['post_count_by_post_status']->trash ) ) {
$hidden_post_count = $data['post_count_by_post_status']->trash;
}
$out = 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-feed-chips' ) ) . '>';
if ( isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$search_term = sanitize_text_field( wp_unslash( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_order = isset( $_GET['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) : 'DESC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// Clear search chip.
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is the search term */ __( 'Clear search for "%s"', 'friends' ), $search_term ) );
$out .= ' ';
// Order chips.
if ( 'DESC' === $current_order ) {
$out .= '
' . esc_html__( 'Oldest first', 'friends' ) . ' ';
} else {
$out .= '
' . esc_html__( 'Oldest first', 'friends' ) . ' ';
$out .= '
' . esc_html__( 'Newest first', 'friends' ) . ' ';
}
} else {
// Post count chips.
$nonce = wp_create_nonce( 'friends_post_counts' );
foreach ( $data['post_count_by_post_format'] as $post_format => $count ) {
$out .= '
' . esc_html( $friends->get_post_format_plural_string( $post_format, $count ) ) . ' ';
}
// Hidden items chip.
if ( isset( $_GET['show-hidden'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$out .= '
' . esc_html__( 'Hide hidden items', 'friends' ) . ' ';
} elseif ( $hidden_post_count > 0 ) {
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is the number of hidden posts */ _n( '%s hidden items', '%s hidden items', $hidden_post_count, 'friends' ), number_format_i18n( $hidden_post_count ) ) );
$out .= ' ';
}
// Reaction chips.
if ( class_exists( __NAMESPACE__ . '\Reactions' ) ) {
foreach ( Reactions::get_available_emojis() as $slug => $reaction ) {
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is an emoji */ __( 'Reacted with %s', 'friends' ), $reaction->char ) );
$out .= ' ';
}
}
}
$out .= '
';
return $out;
}
/**
* Render the friends/welcome block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_welcome_block( $attributes = array() ) {
$friends = Friends::get_instance();
$args = array(
'friends' => $friends,
'post_format' => $friends->frontend->post_format,
);
ob_start();
if ( empty( $attributes['forceWelcome'] ) && User_Query::all_associated_users()->get_total() > 0 ) {
Friends::template_loader()->get_template_part( 'frontend/no-posts', $args['post_format'], $args );
} else {
Friends::template_loader()->get_template_part( 'frontend/no-friends', $args['post_format'], $args );
}
$content = ob_get_clean();
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-welcome friends-empty-state' ) ) . '>' . $content . '
';
}
/**
* Render the friends/post-content block.
*
* Renders friend post content directly without block parsing.
*
* @return string The rendered block HTML.
*/
public function render_post_content_block() {
global $post;
if ( ! $post ) {
return '' . esc_html__( 'Post content will appear here.', 'friends' ) . '
';
}
$content = get_the_content();
$content = wp_kses_post( $content );
$content = wpautop( $content );
$content .= Link_Preview::render();
return '' . $content . '
';
}
/**
* Render the friends/post-permalink block.
*
* Shows "X ago on domain.com Y min read" for each post.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_post_permalink_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
global $post;
if ( ! $post ) {
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-post-permalink' ) ) . '>' . esc_html__( '3 days ago on example.com', 'friends' ) . ' ' . esc_html__( '2 min read', 'friends' ) . '
';
}
$friend_user = User::get_post_author( $post );
if ( ! $friend_user || is_wp_error( $friend_user ) ) {
return '';
}
$author_url = $friend_user->get_local_friends_page_url();
$guid = get_the_guid( $post );
$domain = wp_parse_url( $guid, PHP_URL_HOST );
$local_url = $author_url . $post->ID . '/';
// Reading time.
$read_time_seconds = Frontend::calculate_read_time( get_the_content() );
$read_time = '';
if ( $read_time_seconds >= 60 ) {
$mins = ceil( $read_time_seconds / MINUTE_IN_SECONDS );
/* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
$read_time = sprintf( _n( '%s min', '%s mins', $mins ), $mins ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
} elseif ( $read_time_seconds > 20 ) {
$read_time = _x( '< 1 min', 'reading time', 'friends' );
}
/* translators: %s is a time span */
$time_ago = sprintf( __( '%s ago' ), human_time_diff( get_post_time( 'U', true ) ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
$out = 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-post-permalink' ) ) . '>';
$out .= sprintf(
// translators: %1$s is a date or relative time, %2$s is a site name or domain.
_x( '%1$s on %2$s', 'at-date-on-post', 'friends' ),
'
' . esc_html( $time_ago ) . ' ',
'
' . esc_html( $domain ) . ' '
);
if ( $read_time ) {
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is a timeframe */ __( '%s read', 'friends' ), $read_time ) );
$out .= ' ';
}
$out .= '
';
return $out;
}
/**
* Render the friends/post-reblog block.
*
* @return string The rendered block HTML.
*/
public function render_post_reblog_block() {
global $post;
if ( ! $post ) {
return '' . esc_html_x( 'Reblog', 'button', 'friends' ) . ' ';
}
$friend_user = User::get_post_author( $post );
if ( $friend_user && ! is_wp_error( $friend_user ) && get_current_user_id() === $friend_user->ID ) {
return '';
}
$post_id = get_the_ID();
$reblog_nonce = wp_create_nonce( 'friends-reblog' );
$reblog_status = get_post_meta( $post_id, 'reblogged', true ) ? ' dashicons-saved' : '';
$out = '';
$out .= '';
$out .= ' ' . esc_html_x( 'Reblog', 'button', 'friends' ) . ' ';
$out .= ' ';
$out .= ' ';
return $out;
}
/**
* Render the friends/post-boost block.
*
* @return string The rendered block HTML.
*/
public function render_post_boost_block() {
global $post;
if ( ! $post ) {
return '' . esc_html_x( 'Boost', 'button', 'friends' ) . ' ';
}
if ( ! has_filter( 'friends_boost' ) && ! class_exists( '\Activitypub\Activitypub' ) ) {
return '';
}
$friend_user = User::get_post_author( $post );
if ( $friend_user && ! is_wp_error( $friend_user ) && get_current_user_id() === $friend_user->ID ) {
return '';
}
$post_id = get_the_ID();
$boost_nonce = wp_create_nonce( 'friends-boost' );
$boost_status = get_post_meta( $post_id, 'boosted', true ) ? ' dashicons-saved' : '';
$out = '';
$out .= '';
$out .= ' ' . esc_html_x( 'Boost', 'button', 'friends' ) . ' ';
$out .= ' ';
$out .= ' ';
return $out;
}
/**
* Render the friends/post-reactions block.
*
* @return string The rendered block HTML.
*/
public function render_post_reactions_block() {
global $post;
if ( ! $post ) {
return '⭐ ❤️ ';
}
if ( ! class_exists( __NAMESPACE__ . '\Reactions' ) ) {
return '';
}
$post_id = get_the_ID();
$reactions = Reactions::get_post_reactions();
$reaction_nonce = wp_create_nonce( 'friends-reaction' );
$out = '';
foreach ( $reactions as $slug => $reaction ) {
$pressed = $reaction->user_reacted ? ' pressed' : '';
$out .= '';
$out .= '' . esc_html( $reaction->emoji ) . ' ' . esc_html( $reaction->count );
$out .= ' ';
}
// Render available emojis as inline buttons for reactions not yet used.
$available = Reactions::get_available_emojis();
foreach ( $available as $slug => $emoji ) {
if ( isset( $reactions[ $slug ] ) ) {
continue;
}
$out .= '' . esc_html( $emoji->char ) . ' ';
}
$out .= ' ';
return $out;
}
/**
* Render the friends/post-comments block.
*
* @return string The rendered block HTML.
*/
public function render_post_comments_block() {
global $post;
if ( ! $post ) {
return ''; // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
}
$post_id = get_the_ID();
$has_mention = get_post_meta( $post_id, '_has_mention_in_comments', true );
$comment_icon = $has_mention ? 'format-status' : 'admin-comments';
$comment_text = $has_mention ? __( 'Comments (You were mentioned)', 'friends' ) : __( 'Comments' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
$out = '';
return $out;
}
/**
* Get the current author from the frontend context.
*
* @return User|null
*/
private function get_frontend_author() {
$friends = Friends::get_instance();
return $friends->frontend->author;
}
/**
* Render the friends/author-star block.
*
* @return string The rendered block HTML.
*/
public function render_author_star_block() {
$author = $this->get_frontend_author();
if ( ! $author ) {
return '☆ ';
}
$starred = $author->is_starred();
$star_class = $starred ? 'dashicons-star-filled starred' : 'dashicons-star-empty not-starred';
$star_nonce = wp_create_nonce( 'star-' . $author->user_login );
return ' ';
}
/**
* Render the friends/author-avatar block.
*
* @return string The rendered block HTML.
*/
public function render_author_avatar_block() {
$author = $this->get_frontend_author();
if ( ! $author ) {
return ' ';
}
$avatar_url = $author->get_avatar_url();
if ( ! $avatar_url ) {
return ' ';
}
return ' ';
}
/**
* Render the friends/author-name block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_author_name_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$author = $this->get_frontend_author();
if ( ! $author ) {
return 'get_wrapper_attributes(
array(
'class' => 'wp-block-friends-author-name',
'id' => 'page-title',
)
) . '>' . esc_html__( 'Author Name', 'friends' ) . ' ';
}
return 'get_wrapper_attributes(
array(
'class' => 'wp-block-friends-author-name',
'id' => 'page-title',
)
) . '>' . wp_kses_post( apply_filters( 'friends_author_display_name_html', esc_html( $author->display_name ), $author->display_name, $author ) ) . ' ';
}
/**
* Render the friends/author-description block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_author_description_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$author = $this->get_frontend_author();
if ( ! $author ) {
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-description' ) ) . '>' . esc_html__( 'Author description will appear here.', 'friends' ) . '
';
}
if ( ! $author->description ) {
return '';
}
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-description' ) ) . '>' . wp_kses( $author->description, array( 'a' => array( 'href' => array() ) ) ) . '
';
}
/**
* Render the friends/author-chips block.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
public function render_author_chips_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$friends = Friends::get_instance();
$author = $this->get_frontend_author();
if ( ! $author ) {
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-chips' ) ) . '>' . esc_html__( 'Following', 'friends' ) . ' example.com ' . esc_html__( 'Edit', 'friends' ) . '
';
}
$out = 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-chips' ) ) . '>';
// Role chip.
$out .= '
' . esc_html( $author->get_role_name() ) . ' ';
// Folder chip.
if ( $author instanceof Subscription ) {
$folder = $author->get_folder();
if ( $folder ) {
$out .= '
📁 ' . esc_html( $folder->name ) . ' ';
}
}
// Since chip.
$out .= '
' . esc_html( sprintf( /* translators: %s is a date */ __( 'Since %s', 'friends' ), date_i18n( __( 'F j, Y' ), strtotime( $author->user_registered ) ) ) ) . ' '; // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
// User URL chip.
if ( $author->user_url ) {
$domain = wp_parse_url( $author->user_url, PHP_URL_HOST );
if ( $domain ) {
$domain = preg_replace( '/^www\./', '', $domain );
$out .= '
' . esc_html( $domain ) . ' ';
}
}
// Post count chips.
$post_counts = $author->get_post_count_by_post_format();
foreach ( $post_counts as $post_format => $count ) {
$out .= '
' . esc_html( $friends->get_post_format_plural_string( $post_format, $count ) ) . ' ';
}
// Hidden items chip.
$hidden_post_count = $author->get_post_in_trash_count();
if ( $hidden_post_count > 0 ) {
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is the number of hidden posts */ _n( '%s hidden items', '%s hidden items', $hidden_post_count, 'friends' ), number_format_i18n( $hidden_post_count ) ) );
$out .= ' ';
}
// Reaction chips.
if ( class_exists( __NAMESPACE__ . '\Reactions' ) ) {
foreach ( Reactions::get_available_emojis() as $slug => $reaction ) {
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is an emoji */ __( 'Reacted with %s', 'friends' ), $reaction->char ) );
$out .= ' ';
}
}
// Feed count chip.
$active_feeds = count( $author->get_active_feeds() );
$total_feeds = count( $author->get_feeds() );
if ( $active_feeds > 0 ) {
$out .= '
';
$out .= esc_html( sprintf( /* translators: %s is the number of feeds */ _n( '%s feed', '%s feeds', $active_feeds, 'friends' ), $active_feeds ) );
if ( $total_feeds > $active_feeds ) {
$extra = $total_feeds - $active_feeds;
$out .= ' (+' . esc_html( $extra ) . ' ' . esc_html__( 'more', 'friends' ) . ') ';
}
$out .= ' ';
}
// Folder selector.
if ( $author instanceof Subscription ) {
$folders = Subscription::get_folders();
$current_folder = $author->get_folder();
$current_id = $current_folder ? $current_folder->term_id : 0;
$nonce = wp_create_nonce( 'friends-move-to-folder' );
$out .= '
';
$out .= '📁 ';
$out .= '' . esc_html__( 'No folder', 'friends' ) . ' ';
foreach ( $folders as $folder ) {
$out .= 'term_id, false ) . '>' . esc_html( $folder->name ) . ' ';
}
$out .= '' . esc_html__( '+ New folder', 'friends' ) . ' ';
$out .= ' ';
$out .= ' ';
}
// Edit chip.
$out .= '
' . esc_html__( 'Edit', 'friends' ) . ' ';
// Refresh chip.
$out .= '
' . esc_html__( 'Refresh', 'friends' ) . ' ';
$out .= '
';
return $out;
}
/**
* Render the Friends List block
*
* @param array $attributes Attributes set by Blocks.
* @return string The new block content.
*/
public function render_friends_list_block( $attributes = array() ) {
if ( ! isset( $attributes['user_types'] ) ) {
$attributes['user_types'] = 'subscriptions';
}
if ( 'folders' === $attributes['user_types'] ) {
return $this->render_friends_list_by_folder( $attributes );
}
if ( ! empty( $attributes['folder'] ) ) {
$friends = User_Query::subscriptions_in_folder( intval( $attributes['folder'] ) );
$folder_term = get_term( intval( $attributes['folder'] ), Subscription::TAXONOMY );
$no_users = '';
} else {
$folder_term = null;
switch ( $attributes['user_types'] ) {
case 'starred':
$friends = User_Query::starred_friends_subscriptions();
$no_users = '';
break;
default:
case 'subscriptions':
$friends = User_Query::all_subscriptions();
$no_users = __( "You're not following anyone yet.", 'friends' );
break;
}
}
if ( $friends->get_total() === 0 ) {
if ( ! $no_users ) {
return '';
}
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list no-users' ) ) . '>' . $no_users . ' ';
}
$heading = '';
if ( $folder_term && ! is_wp_error( $folder_term ) ) {
$heading = '📁 ' . esc_html( $folder_term->name ) . ' ';
}
if ( ! empty( $attributes['users_inline'] ) ) {
$out = $heading;
$first = true;
} else {
$out = $heading . 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list' ) ) . '>';
}
$count = 0;
foreach ( $friends->get_results() as $friend_user ) {
++$count;
if ( ! empty( $attributes['users_inline'] ) ) {
if ( ! $first ) {
$out .= ', ';
}
$first = false;
} else {
$out .= '';
}
if ( Friends::has_required_privileges() ) {
$url = $friend_user->get_local_friends_page_url();
} else {
$url = $friend_user->user_url;
}
$out .= sprintf(
'%2$s ',
esc_url( $url ),
esc_html( $friend_user->display_name )
);
if ( empty( $attributes['users_inline'] ) ) {
$out .= ' ';
}
}
if ( empty( $attributes['users_inline'] ) ) {
$out .= ' ';
}
return $out;
}
/**
* Render the friends list grouped by folder hierarchy.
*
* @param array $attributes Block attributes.
* @return string The rendered block HTML.
*/
private function render_friends_list_by_folder( $attributes ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$all = User_Query::all_subscriptions();
$folders = Subscription::get_folders();
if ( $all->get_total() === 0 ) {
return 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list no-users' ) ) . '>' . esc_html__( "You're not following anyone yet.", 'friends' ) . ' ';
}
$out = 'get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list folders' ) ) . '>';
// Render each folder as a collapsible details element.
foreach ( $folders as $folder ) {
$folder_subs = User_Query::subscriptions_in_folder( $folder->term_id );
if ( $folder_subs->get_total() === 0 ) {
continue;
}
$out .= '
';
$out .= '📁 ' . esc_html( $folder->name ) . ' (' . $folder_subs->get_total() . ') ';
$out .= '';
$out .= ' ';
}
// Render unfoldered subscriptions.
$unfoldered = User_Query::unfoldered_subscriptions();
if ( $unfoldered->get_total() > 0 ) {
if ( ! empty( $folders ) ) {
$out .= '
';
$out .= '' . esc_html__( 'Uncategorized', 'friends' ) . ' (' . $unfoldered->get_total() . ') ';
}
$out .= '';
if ( ! empty( $folders ) ) {
$out .= ' ';
}
}
$out .= '
';
return $out;
}
/**
* Render the Friend Posts block
*
* @param array $attributes Attributes set by Blocks.
* @return string The new block content.
*/
public function render_friend_posts_block( $attributes = array() ) {
$date_formats = array(
'Y m d H' => 'human',
'Y m d' => 'H:i',
'Y w' => 'D j, H:i',
'Y' => 'M j',
'' => 'M j, Y',
);
$last_author = false;
$only_users = array();
if ( isset( $attributes['only_users'] ) ) {
$only_users = array_flip( array_filter( preg_split( '/[, ]+/', $attributes['only_users'] ) ) );
}
$exclude_users = array();
if ( isset( $attributes['exclude_users'] ) ) {
$exclude_users = array_flip( array_filter( preg_split( '/[, ]+/', $attributes['exclude_users'] ) ) );
}
$count = max( 1, min( 100, $attributes['count'] ) );
$remaining = $count;
$offset = 0;
$out = '';
while ( $remaining > 0 ) {
$recent_posts = wp_get_recent_posts(
array(
'numberposts' => $count,
'offset' => $offset,
'post_type' => apply_filters( 'friends_frontend_post_types', array() ),
)
);
if ( count( $recent_posts ) === 0 ) {
break;
}
$offset += $count;
foreach ( $recent_posts as $post ) {
$friend_user = new User( $post['post_author'] );
if ( ! empty( $only_users ) && ! isset( $only_users[ $friend_user->user_login ] ) && ! isset( $only_users[ $friend_user->ID ] ) && ! isset( $only_users[ $friend_user->display_name ] ) ) {
continue;
}
if ( ! empty( $exclude_users ) && isset( $exclude_users[ $friend_user->user_login ] ) && isset( $exclude_users[ $friend_user->ID ] ) && isset( $exclude_users[ $friend_user->display_name ] ) ) {
continue;
}
if ( $remaining <= 0 ) {
break 2;
}
--$remaining;
$author = $friend_user->display_name;
if ( $attributes['author_name'] || $attributes['author_avatar'] || $attributes['author_inline'] ) {
if ( $attributes['author_inline'] || $last_author !== $author ) {
if ( $last_author && ! $attributes['author_inline'] ) {
$out .= ' ';
}
$last_author = $author;
$out .= '';
if ( $attributes['author_avatar'] ) {
$out .= ' ';
}
if ( $attributes['author_name'] ) {
$out .= esc_html( $author );
}
if ( $attributes['author_inline'] ) {
$out .= ' ';
} else {
$out .= '';
}
} else {
$out .= ' ';
}
} else {
$out .= ' ';
}
$title = get_the_title( $post['ID'] );
if ( empty( $title ) ) {
$title = get_the_excerpt( $post['ID'] );
}
$out .= sprintf(
'%2$s ',
esc_url( $attributes['internal_link'] ? $friend_user->get_local_friends_page_url( $post['ID'] ) : get_permalink( $post['ID'] ) ),
esc_html( $title )
);
if ( $attributes['show_date'] ) {
$post_date = strtotime( $post['post_date_gmt'] );
foreach ( $date_formats as $compare => $date_format ) {
if ( gmdate( $compare ) === gmdate( $compare, $post_date ) ) {
break;
}
}
$out .= ' ';
if ( 'human' === $date_format ) {
/* translators: %s is a time span */
$out .= sprintf( __( '%s ago' ), human_time_diff( $post_date ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
} else {
$out .= date_i18n( $date_format, strtotime( $post['post_date'] ) );
}
$out .= ' ';
}
$out .= ' ';
}
}
if ( $last_author && ( $attributes['author_name'] || $attributes['author_avatar'] ) && ! $attributes['author_inline'] ) {
$out .= ' ';
}
$out .= '';
return $out;
}
/**
* Enqueue the sidebar blocks editor script.
*/
public function enqueue_sidebar_blocks() {
wp_enqueue_script(
'friends-sidebar-blocks',
plugins_url( 'blocks/sidebar-blocks/index.js', FRIENDS_PLUGIN_FILE ),
array( 'wp-blocks', 'wp-element', 'wp-server-side-render' ),
Friends::VERSION,
true
);
// Pass folder data to the editor for the friends-list block.
$folders = Subscription::get_folders();
$folder_data = array();
foreach ( $folders as $folder ) {
$folder_data[] = array(
'term_id' => $folder->term_id,
'name' => $folder->name,
);
}
wp_localize_script( 'friends-sidebar-blocks', 'friendsFolders', $folder_data );
}
/**
* Hide blocks that were marked as "only visible for friends".
*
* Since the friendship feature was removed, "only-friends" content
* stays hidden to prevent accidental exposure. "not-friends" content
* is shown to everyone.
*
* @param string $content The content provided by the user.
* @param array $block Attributes for the block.
* @return string The rendered content.
*/
public function render_friends_block_visibility( $content, $block ) {
$css_class = ( empty( $block['attrs'] ) || empty( $block['attrs']['className'] ) ) ? '' : $block['attrs']['className'];
if ( preg_match( '/\bonly-friends\b/', $css_class ) ) {
// Content meant only for friends stays hidden since friendships were removed.
if ( ! Friends::has_required_privileges() ) {
return '';
}
}
return $content;
}
/**
* Load up the language data for the Blocks blocks
*/
public function language_data() {
$locale_data = array();
if ( function_exists( 'wp_get_jed_locale_data' ) ) {
$locale_data = wp_get_jed_locale_data( 'friends' );
} elseif ( function_exists( 'blocks_get_jed_locale_data' ) ) {
$locale_data = blocks_get_jed_locale_data( 'friends' );
}
if ( ! empty( $locale_data ) ) {
wp_add_inline_script(
'friends-block-not-friends',
'wp.i18n.setLocaleData( ' . wp_json_encode( $locale_data ) . ', "friends" );',
'before'
);
}
}
}