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 = '
    '; $out .= ''; if ( $subscription['avatar_url'] ) { $out .= ''; } $out .= '' . esc_html( $subscription['display_name'] ) . ''; $out .= ''; $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() ), ) ); ?>

    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 = ''; 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 = '
    get_wrapper_attributes( array( 'class' => 'wp-block-friends-add-subscription' ) ) . '>'; $out .= ''; $out .= esc_html__( 'Follow', 'friends' ); $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 = ''; 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 .= ' '; } // 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 .= ' '; } $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 '💬 ' . esc_html__( 'Comments' ) . ''; // 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 = '
    '; $out .= ''; $out .= ' ' . esc_html( $comment_text ) . ''; $out .= ''; $out .= '
    '; $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 .= ' '; } // 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 . ''; } 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 = ''; } $last_author = $author; $out .= '
  • '; if ( $attributes['author_avatar'] ) { $out .= ''; } if ( $attributes['author_name'] ) { $out .= esc_html( $author ); } if ( $attributes['author_inline'] ) { $out .= ' '; } else { $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' ); } } }