# friends/4.3.2/includes/class-frontend.php

Friends, version 4.3.2. 2,439 lines.

- Page: https://pluginprobe.com/plugins/friends/4.3.2/code/includes/class-frontend.php
- Raw: https://pluginprobe.com/plugins/friends/4.3.2/raw/includes/class-frontend.php
- Modified: 2026-09-15T14:52:52+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/friends/4.3.2/code/includes/class-frontend.php#L10-L20`.

````php
<?php
/**
 * Friends Page
 *
 * This contains the functions for /friends/
 *
 * @package Friends
 */

namespace Friends;

/**
 * This is the class for the /friends/ part of the Friends Plugin.
 *
 * @since 0.6
 *
 * @package Friends
 * @author Alex Kirk
 */
class Frontend {
	/**
	 * Contains a reference to the Friends class.
	 *
	 * @var Friends
	 */
	private $friends;

	/**
	 * Whether we are on the /friends page.
	 *
	 * @var boolean
	 */
	private $is_friends_page = false;

	/**
	 * Whether an author is being displayed
	 *
	 * @var object|false
	 */
	public $author = false;

	/**
	 * Whether an tag is being displayed
	 *
	 * @var object|false
	 */
	public $tag = false;

	/**
	 * Whether a post-format is being displayed
	 *
	 * @var string|false
	 */
	public $post_format = false;

	/**
	 * Whether a specific template was selected to load.
	 *
	 * @var string|false
	 */
	public $template = false;

	/**
	 * Whether a reaction is being displayed.
	 *
	 * @var string|false
	 */
	public $reaction = false;

	/**
	 * The current theme.
	 *
	 * @var string
	 */
	private $theme = 'default';
	/**
	 * Available themes.
	 *
	 * @var array
	 */
	private static $themes = array();

	/**
	 * Constructor
	 *
	 * @param Friends $friends A reference to the Friends object.
	 */
	public function __construct( Friends $friends ) {
		$this->friends = $friends;
		$this->register_hooks();
	}

	/**
	 * Register the WordPress hooks
	 */
	private function register_hooks() {
		add_filter( 'pre_get_posts', array( $this, 'friend_posts_query' ), 2 );
		add_filter( 'pre_get_posts', array( $this, 'exclude_compose_format_from_feed' ), 10 );
		add_filter( 'post_type_link', array( $this, 'friend_post_link' ), 10, 2 );
		add_filter( 'friends_header_widget_title', array( $this, 'header_widget_title' ) );
		add_filter( 'get_edit_post_link', array( $this, 'friend_post_edit_link' ) );
		add_filter( 'template_include', array( $this, 'template_override' ) );
		add_filter( 'wp_loaded', array( $this, 'add_rewrite_rule' ) );
		add_filter( 'init', array( $this, 'register_friends_sidebar' ) );
		add_action( 'init', array( $this, 'add_theme_supports' ) );
		add_action( 'wp_ajax_friends_publish', array( $this, 'ajax_frontend_publish_post' ) );
		add_action( 'wp_ajax_friends-mention-autocomplete', array( $this, 'ajax_mention_autocomplete' ) );
		add_action( 'wp_ajax_friends-change-post-format', array( $this, 'ajax_change_post_format' ) );
		add_action( 'wp_ajax_friends-load-next-page', array( $this, 'ajax_load_next_page' ) );
		add_action( 'wp_ajax_friends-autocomplete', array( $this, 'ajax_autocomplete' ) );
		add_action( 'wp_ajax_friends-set-widget-open-state', array( $this, 'ajax_set_widget_open_state' ) );
		add_action( 'wp_ajax_friends-get-post-counts', array( $this, 'ajax_get_post_counts' ) );
		add_action( 'friends_search_autocomplete', array( $this, 'autocomplete_user_search' ), 10, 2 );
		add_action( 'wp_ajax_friends-star', array( $this, 'ajax_star_friend_user' ) );
		add_action( 'wp_ajax_friends-move-to-folder', array( $this, 'ajax_move_to_folder' ) );
		add_action( 'wp_ajax_friends-create-folder', array( $this, 'ajax_create_folder' ) );
		add_action( 'wp_ajax_friends-load-comments', array( $this, 'ajax_load_comments' ) );
		add_action( 'wp_ajax_friends-reblog', array( $this, 'wp_ajax_reblog' ) );
		add_action( 'friends_post_footer_first', array( $this, 'reblog_button' ) );
		add_filter( 'friends_reblog', array( get_called_class(), 'reblog' ), 10, 3 );
		add_filter( 'friends_unreblog', array( get_called_class(), 'unreblog' ), 10, 2 );
		add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 2 );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
		add_action( 'template_redirect', array( $this, 'load_theme' ) );
		add_action( 'admin_bar_menu', array( $this, 'admin_bar_theme_switcher' ), 100 );
		add_action( 'customize_loaded_components', array( $this, 'ensure_widget_editing' ) );
		add_action( 'friends_load_theme_default', array( $this, 'default_theme' ) );
		add_action( 'friends_load_theme_block', array( $this, 'block_theme' ) );
		add_action( 'friends_load_theme_google-reader', array( $this, 'google_reader_theme' ) );
		add_action( 'friends_load_theme_mastodon', array( $this, 'mastodon_theme' ) );
		add_action( 'friends_load_theme_twitter', array( $this, 'twitter_theme' ) );
		add_action( 'friends_load_themes', array( $this, 'register_block_theme' ) );
		add_action( 'friends_load_themes', array( $this, 'register_google_reader_theme' ) );
		add_action( 'friends_load_themes', array( $this, 'register_mastodon_theme' ) );
		add_action( 'friends_load_themes', array( $this, 'register_twitter_theme' ) );
		add_action( 'init', array( $this, 'register_block_templates' ) );
		add_action( 'friends_template_paths', array( $this, 'friends_template_paths' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_scripts' ), 99999 );
		add_action( 'wp_footer', array( $this, 'dequeue_scripts' ) );
		add_action( 'the_post', array( $this, 'the_post' ), 10, 2 );
		add_action( 'parse_query', array( $this, 'parse_query' ) );
		add_filter( 'body_class', array( $this, 'add_body_class' ) );
		add_filter( 'block_type_metadata_settings', array( $this, 'block_type_metadata_settings' ), 15 );
		add_filter( 'pre_render_block', array( $this, 'render_friends_template_part' ), 10, 2 );
		add_filter( 'pre_get_block_file_template', array( $this, 'get_friends_block_file_template' ), 10, 3 );
		add_filter( 'get_block_templates', array( $this, 'add_friends_template_parts' ), 10, 3 );
		add_filter( 'tag_row_actions', array( $this, 'tag_row_actions' ), 10, 2 );

		add_action( 'friends_after_header', array( $this, 'migration_notification' ) );
		add_action( 'wp_ajax_friends_dismiss_migration_notification', array( $this, 'ajax_dismiss_migration_notification' ) );

		add_filter( 'friends_override_author_name', array( $this, 'override_author_name' ), 10, 3 );
		add_filter( 'friends_friend_posts_query_viewable', array( $this, 'expose_opml' ), 10, 2 );
		add_filter( 'get_comment_link', array( $this, 'friend_post_comment_link' ), 10, 2 );
	}

	/**
	 * We're asking WordPress to handle the title for us.
	 */
	public function add_rewrite_rule() {
		$existing_rules = get_option( 'rewrite_rules' );
		$needs_flush = false;

		$rules = array(
			'^friends/(.*)/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$' => 'index.php?pagename=friends/$matches[1]&feed=$matches[2]',
			'^friends/feed/?$'       => 'index.php?pagename=friends&feed=feed',
			'^friends/(.*)/(\d+)/?$' => 'index.php?pagename=friends/$matches[1]&page=$matches[2]',
			'^friends/(.*)'          => 'index.php?pagename=friends/$matches[1]',
		);

		foreach ( $rules as $rule => $rewrite ) {
			if ( empty( $existing_rules[ $rule ] ) ) {
				$needs_flush = true;
			}
			add_rewrite_rule( $rule, $rewrite, 'top' );
		}

		if ( $needs_flush ) {
			flush_rewrite_rules();
		}
	}

	/**
	 * Run our add_theme_supports if on the frontend.
	 */
	public function add_theme_supports() {
		if ( ! Friends::on_frontend() ) {
			return;
		}

		$this->add_theme_support_title_tag();
		$this->add_theme_support_admin_bar();
	}

	/**
	 * We're asking WordPress to handle the title for us.
	 */
	private function add_theme_support_title_tag() {
		add_theme_support( 'title-tag' );
		add_filter( 'document_title_parts', array( $this, 'modify_page_title' ) );
	}

	/**
	 * Remove the margin-top on the friends page.
	 */
	private function add_theme_support_admin_bar() {
		add_theme_support(
			'admin-bar',
			array(
				'callback' => '__return_false',
			)
		);
	}

	/**
	 * Modify the page title to be output. This function is only invoked on the friends page.
	 *
	 * @param      array $title  The title.
	 *
	 * @return     array  The modified title.
	 */
	public function modify_page_title( $title ) {
		if ( is_single() ) {
			$title['page'] = __( 'Friends', 'friends' );
			$title['site'] = $this->author->display_name;
		} elseif ( $this->author instanceof User ) {
			$title['title'] = __( 'Friends', 'friends' );
			$title['site'] = $this->author->display_name;
		} else {
			$title = array(
				'site' => __( 'Friends', 'friends' ),
			);
		}
		return $title;
	}

	/**
	 * Registers the sidebar for the /friends page.
	 */
	public function register_friends_sidebar() {
		register_sidebar(
			array(
				'name'          => 'Friends Topbar',
				'id'            => 'friends-topbar',
				'before_widget' => '<div class="friends-main-widget">',
				'after_widget'  => '</div>',
				'before_title'  => '<h1>',
				'after_title'   => '</h1>',
			)
		);
		register_sidebar(
			array(
				'name'          => 'Friends Sidebar',
				'id'            => 'friends-sidebar',
				'before_widget' => '<div class="friends-widget">',
				'after_widget'  => '</div>',
				'before_title'  => '<h5>',
				'after_title'   => '</h5>',
			)
		);
		register_sidebar(
			array(
				'name'          => __( 'Friends Sidebar 2 (Twitter theme)', 'friends' ),
				'id'            => 'friends-sidebar-2',
				'description'   => __( 'Shown in the right column of the Twitter theme.', 'friends' ),
				'before_widget' => '<div class="friends-widget">',
				'after_widget'  => '</div>',
				'before_title'  => '<h5>',
				'after_title'   => '</h5>',
			)
		);

		if ( Friends::on_frontend() ) {
			add_action( 'customize_register', '__return_true' );
		}
	}

	public static function ensure_widget_editing( $components ) {
		if ( ! is_array( $components ) ) {
			$components = array();
		}
		$components[] = 'widgets';
		return $components;
	}

	public function load_theme() {
		if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
			return;
		}
		// Handle theme switching via GET parameter.
		if ( isset( $_GET['friends_theme'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$new_theme = sanitize_text_field( wp_unslash( $_GET['friends_theme'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$themes    = self::get_themes();
			if ( isset( $themes[ $new_theme ] ) ) {
				update_user_option( get_current_user_id(), 'friends_frontend_theme', $new_theme );
			}
		}

		$theme = 'default';
		$default_theme = get_user_option( 'friends_frontend_theme', get_current_user_id() );
		if ( $default_theme ) {
			$theme = $default_theme;
		}
		if ( $this->post_format ) {
			$post_type_theme = get_user_option( 'friends_frontend_theme_' . $this->post_format, get_current_user_id() );
			if ( $post_type_theme ) {
				$theme = $post_type_theme;
			}
		}
		if ( ! has_action( 'friends_load_theme_' . $theme ) ) {
			$theme = 'default';
		}
		$this->theme = $theme;
		do_action( 'friends_load_theme_' . $theme );
	}

	public function friends_template_paths( $file_paths ) {
		$backup_file_paths = $file_paths;
		if ( has_filter( 'friends_template_paths_theme_' . $this->theme ) ) {
			$file_paths = apply_filters( 'friends_template_paths_theme_' . $this->theme, $file_paths );
		}
		if ( empty( $file_paths ) ) {
			return $backup_file_paths;
		}

		return $file_paths;
	}

	/**
	 * Get an asset version that changes when the local file changes.
	 *
	 * @param string $file Relative asset path.
	 * @return string Asset version.
	 */
	private function get_asset_version( $file ) {
		$path = dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file;
		if ( file_exists( $path ) ) {
			return Friends::VERSION . '-' . filemtime( $path );
		}

		return Friends::VERSION;
	}

	public function default_theme() {
		$handle = 'friends';
		$file = 'friends.css';
		$version = $this->get_asset_version( $file );
		wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );

		$file = 'friends-default-theme.js';
		wp_enqueue_script( 'friends-default-theme', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $this->get_asset_version( $file ), true );
	}

	/**
	 * Reference our script for the /friends page
	 */
	public function enqueue_scripts() {
		if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
			return;
		}
		global $wp_query;

		$handle = 'friends';
		$file = 'friends.js';
		$version = $this->get_asset_version( $file );
		wp_enqueue_script( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'common', 'jquery', 'wp-util' ), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ), true );

		$query_vars = wp_json_encode( $this->get_minimal_query_vars( $wp_query->query_vars ) );

		$variables = array(
			'emojis_json'            => plugins_url( 'emojis.json', FRIENDS_PLUGIN_FILE ),
			'ajax_url'               => admin_url( 'admin-ajax.php' ),
			'rest_base'              => rest_url( 'friends/v1/' ),
			'rest_nonce'             => wp_create_nonce( 'wp_rest' ),
			'text_link_expired'      => __( 'The link has expired. A new link has been generated, please click it again.', 'friends' ),
			'text_undo'              => __( 'Undo' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
			'text_trash_post'        => __( 'Trash this post', 'friends' ),
			'text_del_convers'       => __( 'Do you really want to delete this conversation?', 'friends' ),
			'text_no_more_posts'     => __( 'No more posts available.', 'friends' ),
			'text_checking_url'      => __( 'Checking URL.', 'friends' ),
			'text_refreshed'         => __( 'Refreshed', 'friends' ),
			'text_refreshing'        => __( 'Refreshing', 'friends' ),
			'text_compact_mode'      => __( 'Compact mode', 'friends' ),
			'text_expanded_mode'     => __( 'Expanded mode', 'friends' ),
			'text_loading_comments'  => __( 'Loading comments...', 'friends' ),
			'text_still_loading'     => __( 'Still loading...', 'friends' ),
			'message_delivery_nonce' => wp_create_nonce( 'friends-message-delivery-statuses' ),
			'refresh_now'            => isset( $_GET['refresh'] ) ? 'true' : 'false', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			'query_vars'             => $query_vars,
			'qv_sign'                => sha1( wp_salt( 'nonce' ) . $query_vars ),
			'current_page'           => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1,
			'max_page'               => $wp_query->max_num_pages,
		);

		// translators: %s is a user handle.
		$variables['text_confirm_delete_follower'] = __( 'Do you really want to delete the follower %s?', 'friends' );
		$variables['text_new_folder']              = __( 'Folder name:', 'friends' );
		$variables['text_loading']                 = __( 'Loading...', 'friends' );
		$variables['text_discovering']             = __( 'Discovering feeds...', 'friends' );
		$variables['text_ready_to_follow']         = __( 'Ready', 'friends' );
		$variables['text_follow']                  = __( 'Follow', 'friends' );
		$variables['text_follow_selected']         = __( 'Follow selected', 'friends' );
		$variables['text_review_selected']         = __( 'Review selected', 'friends' );
		$variables['text_skip']                    = __( 'Skip', 'friends' );
		$variables['text_error']                   = __( 'An error occurred.', 'friends' );
		$variables['text_display_name']            = __( 'Display Name', 'friends' );
		$variables['text_username']                = __( 'Username', 'friends' );
		$variables['text_post_format']             = __( 'Post Format', 'friends' );
		$variables['text_feed_parser']             = __( 'Parser', 'friends' );
		$variables['text_feed_metadata']           = __( 'Display feed metadata', 'friends' );
		$variables['text_hide_feed_metadata']      = __( 'Hide feed metadata', 'friends' );
		$variables['text_preview_feed']            = __( 'Preview feed', 'friends' );
		$variables['text_hide_preview']            = __( 'Hide preview', 'friends' );
		$variables['text_no_preview_items']        = __( 'No preview items were found.', 'friends' );
		$variables['text_add_feed_url']            = __( 'Add feed URL', 'friends' );
		$variables['text_feed_url']                = __( 'Feed URL', 'friends' );
		$variables['text_custom_feed']             = __( 'Custom feed', 'friends' );
		$variables['text_feed_url_required']       = __( 'Please enter a feed URL.', 'friends' );
		$variables['text_show_more_feeds']         = __( 'Show more feeds', 'friends' );
		$variables['text_show_unsupported_feeds']  = __( 'Show unsupported feeds', 'friends' );
		$variables['text_no_supported_feeds']      = __( 'No supported feeds discovered.', 'friends' );
		$variables['text_no_feed_selected']        = __( 'Please select at least one feed.', 'friends' );
		$variables['text_opml_no_feeds']           = __( 'No feeds were found in the OPML file.', 'friends' );
		$variables['text_opml_invalid']            = __( 'Could not parse the OPML file.', 'friends' );
		$variables['text_opml_select_all']         = __( 'Select all', 'friends' );
		$variables['text_opml_deselect_all']       = __( 'Deselect all', 'friends' );
		$variables['text_opml_import_selected']    = __( 'Review selected', 'friends' );
		$variables['text_opml_no_selected']        = __( 'Please select at least one feed.', 'friends' );
		$variables['text_opml_no_feeds_for_url']   = __( 'No feeds discovered.', 'friends' );
		$variables['post_formats']                 = array_merge( array( 'autodetect' => __( 'Autodetect Post Format', 'friends' ) ), get_post_format_strings() );
		$variables['registered_parsers']           = array_map( 'wp_strip_all_tags', $this->friends->feed->get_registered_parsers() );
		wp_localize_script( 'friends', 'friends', $variables );
	}

	public function dequeue_scripts() {
		if ( is_user_logged_in() && Friends::on_frontend() && 'block' !== $this->theme ) {
			// Dequeue theme styles so that they don't interact with the Friends frontend.
			$wp_styles = wp_styles();
			$theme_root_path = wp_parse_url( get_theme_root_uri(), PHP_URL_PATH );
			foreach ( $wp_styles->queue as $style ) {
				$src = $wp_styles->registered[ $style ]->src;
				$src_path = wp_parse_url( $src, PHP_URL_PATH );
				$is_theme_style = $theme_root_path && $src_path
					&& 0 === strpos( $src_path, trailingslashit( $theme_root_path ) );
				if ( 'global-styles' === $style || $is_theme_style ) {
					wp_dequeue_style( $style );
				}
			}
		}
	}

	public function migration_notification() {
		if ( ! Friends::on_frontend() ) {
			return;
		}
		if ( ! Friends::has_pending_migrations() ) {
			return;
		}
		?>
		<div class="friends-migration-notification">
			<?php esc_html_e( 'Welcome to Friends 4.0!', 'friends' ); ?>
			<?php esc_html_e( 'Some data migrations are still pending.', 'friends' ); ?>
			<a href="<?php echo esc_url( admin_url( 'admin.php?page=friends' ) ); ?>"><?php esc_html_e( "Learn what's new \u{2192}", 'friends' ); ?></a>
			<span class="friends-migration-notification-meta">
				<small class="friends-migration-reappear" hidden><?php esc_html_e( 'Will reappear in 2 days.', 'friends' ); ?></small>
				<button id="friends-dismiss-migration-notification" data-nonce="<?php echo esc_attr( wp_create_nonce( 'friends-dismiss-migration-notification' ) ); ?>"><?php esc_html_e( 'Dismiss', 'friends' ); ?></button>
			</span>
		</div>
		<script>
		document.getElementById( 'friends-dismiss-migration-notification' ).addEventListener( 'click', function() {
			var btn = this;
			var notification = btn.closest( '.friends-migration-notification' );
			btn.hidden = true;
			notification.querySelector( '.friends-migration-reappear' ).hidden = false;
			fetch( <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>, {
				method: 'POST',
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
				body: 'action=friends_dismiss_migration_notification&_ajax_nonce=' + encodeURIComponent( btn.dataset.nonce ),
			} ).then( function() {
				notification.remove();
			} );
		} );
		</script>
		<?php
	}

	public function ajax_dismiss_migration_notification() {
		check_ajax_referer( 'friends-dismiss-migration-notification' );
		update_option( 'friends_migration_status', time(), false );
		wp_send_json_success();
	}

	public function the_post( $post, $query ) {
		Subscription::set_authordata_by_query( $query );
	}

	public function parse_query( $query ) {
		Subscription::set_authordata_by_query( $query );
	}

	/**
	 * Add a CSS class to the body
	 *
	 * @param array $classes The existing CSS classes.
	 * @return array The modified CSS classes.
	 */
	public function add_body_class( $classes ) {
		if ( $this->friends->on_frontend() ) {
			$classes[] = 'friends-page';
			$classes[] = 'off-canvas';
			$classes[] = 'off-canvas-sidebar-show';
		}

		return $classes;
	}

	public function register_google_reader_theme( Frontend $friends_frontend ) {
		$friends_frontend->register_theme( 'Google Reader', 'google-reader' );
		add_filter(
			'friends_theme_name',
			function ( $name, $slug ) {
				return 'google-reader' === $slug ? __( 'Google Reader', 'friends' ) : $name;
			},
			10,
			2
		);
	}

	public function register_mastodon_theme( Frontend $friends_frontend ) {
		$friends_frontend->register_theme( 'Mastodon', 'mastodon' );
		add_filter(
			'friends_theme_name',
			function ( $name, $slug ) {
				return 'mastodon' === $slug ? __( 'Mastodon', 'friends' ) : $name;
			},
			10,
			2
		);
	}

	public function mastodon_theme() {
		$handle  = 'friends-mastodon';
		$file    = 'templates/mastodon/mastodon.css';
		$version = $this->get_asset_version( $file );
		wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );

		$file = 'templates/mastodon/mastodon.js';
		wp_enqueue_script( 'friends-mastodon', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $this->get_asset_version( $file ), true );
		wp_localize_script(
			'friends-mastodon',
			'friendsMastodon',
			array(
				'mentionNonce' => wp_create_nonce( 'friends-mention-autocomplete' ),
			)
		);

		add_filter(
			'friends_template_paths_theme_mastodon',
			function ( $file_paths ) {
				$file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/mastodon/';
				return $file_paths;
			}
		);
	}

	public function register_twitter_theme( Frontend $friends_frontend ) {
		$friends_frontend->register_theme( 'Twitter', 'twitter' );
		add_filter(
			'friends_theme_name',
			function ( $name, $slug ) {
				return 'twitter' === $slug ? __( 'Twitter', 'friends' ) : $name;
			},
			10,
			2
		);
	}

	public function twitter_theme() {
		$handle  = 'friends-twitter';
		$file    = 'templates/twitter/twitter.css';
		$version = $this->get_asset_version( $file );
		wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );

		$file = 'templates/twitter/twitter.js';
		wp_enqueue_script( 'friends-twitter', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $this->get_asset_version( $file ), true );
		wp_localize_script(
			'friends-twitter',
			'friendsTwitter',
			array(
				'mentionNonce' => wp_create_nonce( 'friends-mention-autocomplete' ),
			)
		);

		add_filter(
			'friends_template_paths_theme_twitter',
			function ( $file_paths ) {
				$file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/twitter/';
				return $file_paths;
			}
		);
	}

	public function google_reader_theme() {
		$handle  = 'friends-google-reader';
		$file    = 'templates/google-reader/google-reader.css';
		$version = $this->get_asset_version( $file );
		wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );

		$file = 'templates/google-reader/google-reader.js';
		wp_enqueue_script( 'friends-google-reader', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery' ), $this->get_asset_version( $file ), true );

		add_filter(
			'friends_template_paths_theme_google-reader',
			function ( $file_paths ) {
				// Add Google Reader templates at highest priority (lowest number), fall back to defaults.
				$file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/google-reader/';
				return $file_paths;
			}
		);
	}

	/**
	 * Add a theme switcher submenu to the Friends admin bar menu.
	 *
	 * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance.
	 */
	public function admin_bar_theme_switcher( $wp_admin_bar ) {
		if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
			return;
		}

		$current_theme = get_user_option( 'friends_frontend_theme', get_current_user_id() );
		if ( ! $current_theme ) {
			$current_theme = 'default';
		}

		$themes = self::get_themes();

		$wp_admin_bar->add_node(
			array(
				'parent' => 'friends-menu',
				'id'     => 'friends-theme',
				'title'  => esc_html__( 'Theme', 'friends' ),
				'href'   => admin_url( 'admin.php?page=friends-settings' ),
			)
		);

		$wp_admin_bar->add_group(
			array(
				'parent' => 'friends-theme',
				'id'     => 'friends-theme-list',
			)
		);

		foreach ( $themes as $slug => $name ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'friends-theme-list',
					'id'     => 'friends-theme-' . $slug,
					'title'  => ( $slug === $current_theme ? '✓ ' : '' ) . esc_html( $name ),
					'href'   => add_query_arg( 'friends_theme', $slug ),
				)
			);
		}
	}

	public function register_block_theme( Frontend $friends_frontend ) {
		$friends_frontend->register_theme( 'Block Theme', 'block' );
		add_filter(
			'friends_theme_name',
			function ( $name, $slug ) {
				return 'block' === $slug ? __( 'Block Theme', 'friends' ) : $name;
			},
			10,
			2
		);
	}

	public function block_theme() {
		// No theme swap needed; templates are registered as plugin templates
		// and injected via template_override.
		$handle  = 'friends-blocks';
		$file    = 'friends-blocks.css';
		$version = $this->get_asset_version( $file );
		wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
	}

	/**
	 * Register Friends block templates as plugin templates.
	 */
	public function register_block_templates() {
		if ( ! wp_is_block_theme() || ! class_exists( 'WP_Block_Templates_Registry' ) ) {
			return;
		}

		$registry  = \WP_Block_Templates_Registry::get_instance();
		$templates = array(
			'friends//friends-index'         => array(
				'title'   => __( 'Friends Feed', 'friends' ),
				'content' => 'index',
			),
			'friends//friends-author-index'  => array(
				'title'   => __( 'Friends Author Feed', 'friends' ),
				'content' => 'friends-author-index',
			),
			'friends//friends-followers'     => array(
				'title'   => __( 'Friends Followers', 'friends' ),
				'content' => 'friends-followers',
			),
			'friends//friends-add-friend'    => array(
				'title'   => __( 'Friends Add Friend', 'friends' ),
				'content' => 'friends-add-friend',
			),
			'friends//friends-subscriptions' => array(
				'title'   => __( 'Friends Following', 'friends' ),
				'content' => 'friends-subscriptions',
			),
			'friends//friends-single'        => array(
				'title'   => __( 'Friends Single Post', 'friends' ),
				'content' => 'single-friend_post_cache',
			),
		);

		foreach ( $templates as $name => $args ) {
			$file = FRIENDS_PLUGIN_DIR . 'themes/friends/templates/' . $args['content'] . '.html';
			if ( file_exists( $file ) ) {
				$registry->register(
					$name,
					array(
						'title'   => $args['title'],
						'content' => file_get_contents( $file ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
					)
				);
			}
		}
	}

	/**
	 * Render template parts for the friends plugin.
	 *
	 * @param string|null $pre_render The pre-rendered content.
	 * @param array       $parsed_block The parsed block.
	 * @return string|null The rendered content or null.
	 */
	public function render_friends_template_part( $pre_render, $parsed_block ) {
		if ( 'core/template-part' !== $parsed_block['blockName'] ) {
			return $pre_render;
		}

		$attrs = $parsed_block['attrs'];
		if ( ! isset( $attrs['theme'] ) || 'friends' !== $attrs['theme'] || ! isset( $attrs['slug'] ) ) {
			return $pre_render;
		}

		// Check for user-customized version in the database first.
		$custom_query = new \WP_Query(
			array(
				'post_type'      => 'wp_template_part',
				'post_status'    => 'publish',
				'post_name__in'  => array( $attrs['slug'] ),
				'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
					array(
						'taxonomy' => 'wp_theme',
						'field'    => 'name',
						'terms'    => 'friends',
					),
				),
				'posts_per_page' => 1,
				'no_found_rows'  => true,
			)
		);

		if ( $custom_query->have_posts() ) {
			$content = $custom_query->posts[0]->post_content;
		} else {
			$file = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/' . $attrs['slug'] . '.html';
			if ( ! file_exists( $file ) ) {
				return $pre_render;
			}
			$content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		}

		$tag = isset( $attrs['tagName'] ) ? $attrs['tagName'] : 'div';

		return '<' . $tag . ' class="wp-block-template-part">' . do_blocks( $content ) . '</' . $tag . '>';
	}

	/**
	 * Build a WP_Block_Template for a friends template part.
	 *
	 * @param string $slug The template part slug.
	 * @param string $file The file path.
	 * @return WP_Block_Template
	 */
	private function build_friends_template_part( $slug, $file ) {
		$template                 = new \WP_Block_Template();
		$template->id             = 'friends//' . $slug;
		$template->theme          = 'friends';
		$template->plugin         = 'friends';
		$template->slug           = $slug;
		$template->source         = 'plugin';
		$template->origin         = 'plugin';
		$template->type           = 'wp_template_part';
		$template->title          = ucwords( str_replace( '-', ' ', $slug ) );
		$template->status         = 'publish';
		$template->has_theme_file = true;
		$template->is_custom      = false;
		$template->content        = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$template->area           = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;

		return $template;
	}

	/**
	 * Resolve a friends template part by ID.
	 *
	 * @param WP_Block_Template|null $block_template The found block template.
	 * @param string                 $id             Template unique identifier.
	 * @param string                 $template_type  Template type.
	 * @return WP_Block_Template|null
	 */
	public function get_friends_block_file_template( $block_template, $id, $template_type ) {
		if ( 'wp_template_part' !== $template_type ) {
			return $block_template;
		}

		$parts = explode( '//', $id, 2 );
		if ( count( $parts ) < 2 || 'friends' !== $parts[0] ) {
			return $block_template;
		}

		$slug = $parts[1];
		$file = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/' . $slug . '.html';
		if ( ! file_exists( $file ) ) {
			return $block_template;
		}

		return $this->build_friends_template_part( $slug, $file );
	}

	/**
	 * Add friends template parts to block template queries.
	 *
	 * @param WP_Block_Template[] $query_result Array of found block templates.
	 * @param array               $query        Arguments to retrieve templates.
	 * @param string              $template_type Template type.
	 * @return WP_Block_Template[]
	 */
	public function add_friends_template_parts( $query_result, $query, $template_type ) {
		if ( 'wp_template_part' !== $template_type ) {
			return $query_result;
		}

		$parts_dir = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/';
		if ( ! is_dir( $parts_dir ) ) {
			return $query_result;
		}

		$slugs_to_include = isset( $query['slug__in'] ) ? $query['slug__in'] : array();

		foreach ( glob( $parts_dir . '*.html' ) as $file ) {
			$slug = basename( $file, '.html' );

			if ( $slugs_to_include && ! in_array( $slug, $slugs_to_include, true ) ) {
				continue;
			}

			// Don't add if already in results.
			$exists = false;
			foreach ( $query_result as $existing ) {
				if ( $existing->slug === $slug && 'friends' === $existing->theme ) {
					$exists = true;
					break;
				}
			}
			if ( ! $exists ) {
				$query_result[] = $this->build_friends_template_part( $slug, $file );
			}
		}

		return $query_result;
	}

	public function block_type_metadata_settings( $settings ) {
		if ( ! Friends::on_frontend() || ! isset( $settings['name'] ) ) {
			return $settings;
		}
		if ( 'core/post-author-name' === $settings['name'] ) {
			$settings['render_callback'] = function ( $attributes, $content, $block ) {
				if ( isset( $block->context['postId'] ) ) {
					$author = User::get_post_author( get_post( $block->context['postId'] ) );
				} else {
					return '';
				}
				if ( empty( $author ) || is_wp_error( $author ) ) {
					return '';
				}

				$author_name      = $author->display_name;
				$author_name_html = apply_filters( 'friends_author_display_name_html', esc_html( $author_name ), $author_name, $author );
				$override_author_name = apply_filters( 'friends_override_author_name', '', $author_name, $block->context['postId'] );
				if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) {
					$author_name_html = sprintf( '<a href="%1$s" target="%2$s" class="wp-block-post-author-name__link">%3$s</a>', esc_url( $author->get_local_friends_page_url() ), esc_attr( $attributes['linkTarget'] ), $author_name_html );
				}

				if ( $override_author_name && trim( str_replace( $override_author_name, '', $author_name ) ) === $author_name ) {
					$author_name_html .= ' – ' . esc_html( $override_author_name );
				}

				$classes = array();
				if ( isset( $attributes['textAlign'] ) ) {
					$classes[] = 'has-text-align-' . $attributes['textAlign'];
				}
				if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
					$classes[] = 'has-link-color';
				}
				$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) );

				return sprintf(
					'<div %1$s>%2$s</div>',
					$wrapper_attributes,
					wp_kses(
						$author_name_html,
						array_merge(
							wp_kses_allowed_html( 'post' ),
							array(
								'a' => array(
									'class'  => true,
									'href'   => true,
									'target' => true,
								),
							)
						)
					)
				);
			};
		}
		return $settings;
	}
	public function tag_row_actions( $actions, $tag ) {
		$actions['view-friends'] = sprintf(
			'<a href="%s">%s</a>',
			esc_url( home_url( '/friends/tag/' . $tag->name ) ),
			__( 'View on your Friends page', 'friends' )
		);
		return $actions;
	}


	/**
	 * Gets the minimal query variables.
	 *
	 * @param      array $query_vars  The query variables.
	 *
	 * @return     array  The minimal query variables.
	 */
	private function get_minimal_query_vars( $query_vars ) {
		return array_filter( array_intersect_key( $query_vars, array_flip( array( 'p', 'page_id', 'pagename', 'author', 'author__not_in', 'post_type', 'post_status', 'posts_per_page', 'order', 'tax_query', 's' ) ) ) );
	}

	public function wp_ajax_reblog() {
		if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['post_id'] ) ) {
			wp_send_json_error( 'error' );
		}

		check_ajax_referer( 'friends-reblog' );

		$post = get_post( intval( $_POST['post_id'] ) );
		if ( ! $post || ! Friends::check_url( $post->guid ) ) {
			wp_send_json_error( 'unknown-post', array( 'guid' => $post->guid ) );
		}
		$reblog_post_id = get_post_meta( get_the_ID(), 'reblogged', true );
		if ( $reblog_post_id ) {
					wp_send_json_success(
						array(
							'post_id'  => $reblog_post_id,
							'redirect' => get_edit_post_link( $reblog_post_id, 'js' ),
						)
					);

			wp_send_json_error( 'already-reblogged', array( 'guid' => $post->guid ) );
			return;
		}

		/**
		 * Reblogs a post
		 *
		 * @param int|null $reblog_post_id The post ID of the reblogged post. Default null.
		 * @param WP_Post  $post           The post object.
		 */
		$reblog_post_id = apply_filters( 'friends_reblog', null, $post, 'draft' );
		if ( ! $reblog_post_id || is_wp_error( $reblog_post_id ) ) {
			wp_send_json_error( 'error' );
		}

		wp_send_json_success(
			array(
				'post_id'  => $reblog_post_id,
				'redirect' => get_edit_post_link( $reblog_post_id, 'js' ),
			)
		);
	}

	public function reblog_button() {
		Friends::template_loader()->get_template_part(
			'frontend/parts/reblog-button',
			null,
			array()
		);
	}

	public static function reblog( $ret, $post, $post_status = 'publish' ) {
		if ( ! $post ) {
			return $ret;
		}
		$post = get_post( $post );
		if ( ! $post ) {
			return $ret;
		}

		$author = get_post_meta( $post->ID, 'author', true );
		if ( ! $author ) {
			$friend = User::get_post_author( $post );
			$author = $friend->display_name;
		}

		$reblog  = '<!-- wp:paragraph {"className":"friends-reblog"} -->' . PHP_EOL . '<p class="friends-reblog">';
		$reblog .= sprintf(
			// translators: %s is a link.
			__( 'Reblog via %s', 'friends' ),
			'<a href="' . esc_url( $post->guid ) . '">' . esc_html( $author ) . '</a>'
		);

		$reblog_title = sprintf(
			// translators: %1$s is the author, %2$s is the post title.
			__( 'Reblog of %1$s: %2$s', 'friends' ),
			$author,
			$post->post_title
		);

		$reblog .= PHP_EOL . '</p>' . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;

		// Wrap the post content in a quote block.
		$reblog .= '<!-- wp:quote {"className":"friends-reblog"} -->' . PHP_EOL . '<blockquote class="wp-block-quote friends-reblog">';
		$reblog .= $post->post_content;
		$reblog .= '</blockquote>' . PHP_EOL . '<!-- /wp:quote -->';

		$new_post = array(
			'post_title'   => $reblog_title,
			'post_author'  => get_current_user_id(),
			'post_status'  => $post_status,
			'post_type'    => 'post',
			'post_format'  => get_post_format( $post ),
			'post_content' => $reblog,
		);
		/**
		 * Allows changing a reblog post before it gets posted.
		 *
		 * @param array $new_post A post array to be handed to wp_insert_post.
		 *
		 * Additionally, the array contains the post_format which can also be changed.
		 *
		 * Example:
		 * ```php
		 * add_filter( 'friends_reblog_pre_insert_post', function( $new_post ) {
		 *     $new_post['post_type'] = 'custom_post_type';
		 *     $new_post['post_format'] = 'aside'; // always set the post_format to aside, regardless of the original post format.
		 *     return $new_post;
		 * } );
		 * ```
		 */
		$new_post = apply_filters( 'friends_reblog_pre_insert_post', $new_post );
		$new_post_id = wp_insert_post( $new_post );

		set_post_format( $new_post_id, $new_post['post_format'] );
		update_post_meta( $new_post_id, 'reblog', $post->guid );
		update_post_meta( $new_post_id, 'reblog_of', $post->ID );
		update_post_meta( $post->ID, 'reblogged', $new_post_id );
		update_post_meta( $post->ID, 'reblogged_by', $new_post['post_author'] );

		return $new_post_id;
	}

	public static function unreblog( $ret, $post ) {
		if ( ! $post ) {
			return $ret;
		}
		$post = get_post( $post );
		if ( ! $post ) {
			return $ret;
		}

		$reblogged = get_post_meta( $post->ID, 'reblogged', true );
		if ( ! $reblogged ) {
			return $ret;
		}

		wp_trash_post( $reblogged );

		return $reblogged;
	}

	/**
	 * The Ajax function to autocomplete @mentions in the compose box.
	 */
	public function ajax_mention_autocomplete() {
		check_ajax_referer( 'friends-mention-autocomplete' );

		if ( ! current_user_can( Friends::REQUIRED_ROLE ) ) {
			wp_send_json_error();
			exit;
		}

		if ( ! isset( $_POST['q'] ) ) {
			wp_send_json_success( array() );
			exit;
		}

		$q       = sanitize_text_field( wp_unslash( $_POST['q'] ) );
		$users   = User_Query::search( '*' . $q . '*' );
		$results = array();

		foreach ( $users->get_results() as $friend ) {
			$handle = null;

			foreach ( $friend->get_feeds() as $feed ) {
				$ap_actor_id = $feed->get_ap_actor_id();
				if ( $ap_actor_id && class_exists( '\Activitypub\Collection\Remote_Actors' ) ) {
					$acct = \Activitypub\Collection\Remote_Actors::get_acct( $ap_actor_id );
					if ( $acct ) {
						$handle = $acct;
						break;
					}
				}
			}

			if ( ! $handle ) {
				$handle = $friend->user_login;
				if ( $friend->user_url ) {
					$host = wp_parse_url( $friend->user_url, PHP_URL_HOST );
					if ( $host ) {
						$handle = $friend->user_login . '@' . $host;
					}
				}
			}

			$results[] = array(
				'handle'       => $handle,
				'display_name' => $friend->display_name,
				'avatar'       => $friend->get_avatar_url(),
			);
		}

		wp_send_json_success( $results );
	}

	/**
	 * The Ajax function to be called upon posting from /friends
	 */
	public function ajax_frontend_publish_post() {
		if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'friends_publish' ) ) {
			return false;
		}
		$p = array(
			'post_type'    => 'post',
			'post_title'   => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
			'post_content' => isset( $_POST['content'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content'] ) ) : '',
			'post_status'  => isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '',
			'post_format'  => isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : '',
		);

		if ( empty( $p['post_status'] ) ) {
			$p['post_status'] = 'publish';
		}
		$result = 'empty';

		if ( ! empty( $_POST['in_reply_to'] ) ) {
			$p['meta_input'] = array(
				'activitypub_in_reply_to' => sanitize_text_field( wp_unslash( $_POST['in_reply_to'] ) ),
			);
		}

		if ( ! empty( $p['post_content'] ) || ! empty( $p['post_title'] ) ) {
			$post_id = wp_insert_post( $p );
			if ( ! empty( $p['post_format'] ) ) {
				set_post_format( $post_id, $p['post_format'] );
			}
			$result = is_wp_error( $post_id ) ? 'error' : 'success';
		}
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest' ) {
			echo esc_html( $result );
			exit;
		}

		if ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {
			wp_safe_redirect( remove_query_arg( 'in_reply_to', add_query_arg( 'result', $result, $_SERVER['HTTP_REFERER'] ) ) );
			exit;
		}
		wp_safe_redirect( home_url( '/friends/' ) );
		exit;
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	}

	/**
	 * The Ajax function to change the post format from the Frontend.
	 */
	public function ajax_change_post_format() {
		$post_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
		$post_format = isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : 'standard';

		check_ajax_referer( "friends-change-post-format_$post_id" );

		if ( ! current_user_can( Friends::REQUIRED_ROLE, $post_id ) ) {
			wp_send_json_error();
			exit;
		}

		$post_formats = get_post_format_strings();
		if ( ! isset( $post_formats[ $post_format ] ) ) {
			wp_send_json_error();
			exit;
		}

		if ( ! set_post_format( $post_id, $post_format ) ) {
			wp_send_json_error();
			exit;
		}

		wp_send_json_success();
	}

	/**
	 * Calculates an estimating read time.
	 *
	 * @param      string $original_text  The original text.
	 *
	 * @return     float  The read time in seconds.
	 */
	public static function calculate_read_time( $original_text ) {
		// from wp_trim_words().
		$text = wp_strip_all_tags( $original_text );

		/*
		 * translators: If your word count is based on single characters (e.g. East Asian characters),
		 * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
		 * Do not translate into your own language.
		 */
		if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) { // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
			$text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' );
			preg_match_all( '/./u', $text, $words_array );
			$words_array = array_shift( $words_array );
			$words_per_minute = 500;
		} else {
			$words_array = preg_split( "/[\n\r\t ]+/", $text, -1, PREG_SPLIT_NO_EMPTY );
			$words_per_minute = 200;
		}

		$additional_time = 0;
		$figures = substr_count( strtolower( $original_text ), '<figure' );
		for ( $i = 0; $i < $figures; $i++ ) {
			if ( $i < 10 ) {
				$additional_time += 12 - $i;
			} else {
				$additional_time += 3;
			}
		}

		return count( $words_array ) / $words_per_minute * 60 + $additional_time;
	}

	public function register_theme( $name, $slug ) {
		self::$themes[ $slug ] = $name;
	}

	public static function get_themes() {
		$themes = array_merge(
			array(
				'default' => __( 'Friends Default Theme', 'friends' ),
			),
			self::$themes
		);

		foreach ( $themes as $slug => $name ) {
			$themes[ $slug ] = apply_filters( 'friends_theme_name', $name, $slug );
		}

		return $themes;
	}
	/**
	 * Handles the post loop on the Friends page.
	 */
	public static function have_posts() {
		$friends = Friends::get_instance();
		$known_author = $friends->frontend->author;
		$known_avatar = null;
		if ( $known_author instanceof User ) {
			$known_avatar = $known_author->get_avatar_url();
		}
		while ( have_posts() ) {
			global $post;
			the_post();
			$args = array(
				'friends' => $friends,
			);
			$args['item_friend_user'] = User::get_post_author( $post );

			if ( $known_author ) {
				$args['friend_user'] = $known_author;
				$args['avatar'] = $known_avatar;
			} else {
				$args['friend_user'] = $args['item_friend_user'];
				$args['avatar'] = $args['friend_user']->get_avatar_url();
			}

			$read_time = self::calculate_read_time( get_the_content() );
			if ( $read_time >= 60 ) {
				$mins = ceil( $read_time / MINUTE_IN_SECONDS );
				/* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
				$args['read_time'] = sprintf( _n( '%s min', '%s mins', $mins ), $mins ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
			} elseif ( $read_time > 20 ) {
				/* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
				$args['read_time'] = _x( '< 1 min', 'reading time', 'friends' );
			}

			Friends::template_loader()->get_template_part(
				'frontend/parts/content',
				get_post_format(),
				$args
			);
		}
	}

	/**
	 * The Ajax function to load more posts for infinite scrolling.
	 */
	public function ajax_load_next_page() {
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( ! isset( $_POST['query_vars'] ) || ! isset( $_POST['page'] ) || ! isset( $_POST['qv_sign'] ) ) {
			wp_send_json_error();
			exit;
		}
		$query_vars = wp_unslash( $_POST['query_vars'] );
		// We do have a nonce verification here.
		if ( sha1( wp_salt( 'nonce' ) . $query_vars ) !== $_POST['qv_sign'] ) {
			wp_send_json_error();
			exit;
		}

		$query_vars = json_decode( $query_vars, true );
		$query_vars['paged'] = intval( $_POST['page'] ) + 1;
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		// We actually want to render the query loop, so we use query_posts.
		query_posts( $query_vars ); // phpcs:ignore WordPress.WP.DiscouragedFunctions.query_posts_query_posts
		ob_start();
		if ( have_posts() ) {
			self::have_posts();
		} else {
			esc_html_e( 'No further posts of your friends could were found.', 'friends' );
		}

		$posts = ob_get_contents();
		ob_end_clean();

		wp_send_json_success( $posts );
	}

	/**
	 * The Ajax function to autocomplete search.
	 */
	public function ajax_autocomplete() {
		check_ajax_referer( 'friends-autocomplete' );
		if ( ! isset( $_POST['q'] ) ) {
			wp_send_json_error();
			exit;
		}

		$q = sanitize_text_field( wp_unslash( $_POST['q'] ) );
		$results = array();

		/**
			$result = '<a href="' . esc_url( add_query_arg( 'name', $q, admin_url( 'admin.php?page=add-friend' ) ) ) . '" class="has-icon-left">';
			$result .= '<span class="ab-icon dashicons dashicons-businessperson"><span class="ab-icon dashicons dashicons-plus"></span></span>';
			$result .= 'Add friend';
			$result .= ' <small>';
			$result .= esc_html( $q );
			$result .= '</small></a>';
			$results[] = $result;
		*/
		$results = apply_filters( 'friends_search_autocomplete', $results, $q );

		$result = '<a href="' . esc_url( add_query_arg( 's', $q, home_url( '/friends/' ) ) ) . '" class="has-icon-left">';
		$result .= '<span class="ab-icon dashicons dashicons-search"></span>';
		$result .= 'Search for ';
		$result .= ' <small>';
		$result .= esc_html( $q );
		$result .= '</small></a>';
		$results[] = $result;

		wp_send_json_success( '<li class="menu-item">' . implode( '</li><li class="menu-item">', $results ) . '</li>' );
	}

	/**
	 * The Add user search entries to the autocomplete results.
	 *
	 * @param      array  $results  The autocomplete results.
	 * @param      string $q        The query.
	 *
	 * @return     array  The autocomplete results.
	 */
	public function autocomplete_user_search( $results, $q ) {
		$users = User_Query::search( '*' . $q . '*' );

		foreach ( $users->get_results() as $friend ) {
			$result = '<a href="' . esc_url( $friend->get_local_friends_page_url() ) . '" class="has-icon-left">';
			$result .= '<span class="ab-icon dashicons dashicons-businessperson"></span>';
			$result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->display_name );
			$result .= ' <small>';
			$result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->user_login );
			$result .= '</small></a>';
			$results[] = $result;
		}

		return $results;
	}

	/**
	 * Filter comment links to use ActivityPub source URL in AJAX context.
	 *
	 * The ActivityPub plugin skips its filter when is_admin() is true,
	 * but AJAX requests go through admin-ajax.php where is_admin() returns true.
	 *
	 * @param string     $comment_link The comment permalink.
	 * @param WP_Comment $comment      The comment object.
	 * @return string The filtered comment link.
	 */
	public function activitypub_comment_link( $comment_link, $comment ) {
		if ( ! $comment || 'comment' !== $comment->comment_type ) {
			return $comment_link;
		}

		$source_url = get_comment_meta( $comment->comment_ID, 'source_url', true );
		if ( $source_url ) {
			return $source_url;
		}

		return $comment_link;
	}

	/**
	 * The Ajax function to load comments.
	 */
	public function ajax_load_comments() {
		if ( ! isset( $_POST['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			wp_send_json_error();
			exit;
		}

		$post_id = intval( $_POST['post_id'] );
		check_ajax_referer( "comments-$post_id" );

		$comments = apply_filters(
			'friends_get_comments',
			get_comments(
				array(
					'post_id' => $post_id,
					'status'  => 'approve',
					'order'   => 'ASC',
				)
			),
			$post_id
		);

		$friend_user = User::get_post_author( get_post( $post_id ) );
		$user_feed_url = get_post_meta( $post_id, 'feed_url', true );
		$user_feed = $this->friends->feed->get_user_feed_by_url( $user_feed_url );

		remove_all_filters( 'comment_form_before' );
		remove_all_filters( 'comment_form_after' );

		// Add filter to fix ActivityPub comment links in AJAX context.
		add_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20, 2 );

		if ( empty( $comments ) ) {
			$content = apply_filters( 'friends_no_comments_feed_available', __( 'No comments yet.', 'friends' ), $post_id, $friend_user, $user_feed );
		} else {
			$template_loader = Friends::template_loader();
			ob_start();
			?>
			<h5><?php esc_html_e( 'Comments' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></h5>
			<ol class="comment-list">
				<?php
					wp_list_comments(
						array(
							'style'       => 'ol',
							'short_ping'  => true,
							'avatar_size' => 24,
						),
						$comments
					);
				?>
			</ol><!-- .comment-list -->
			<?php
			$content = ob_get_contents();
			ob_end_clean();
		}

		remove_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20 );

		$content = apply_filters( 'friends_comments_content', $content, $post_id, $friend_user, $user_feed );

		wp_send_json_success( $content );
	}

	public function ajax_star_friend_user() {
		if ( ! isset( $_POST['friend_id'] ) || ! isset( $_POST['starred'] ) ) {
			wp_send_json_error();
			exit;
		}

		$friend_id = sanitize_text_field( wp_unslash( $_POST['friend_id'] ) );
		check_ajax_referer( "star-$friend_id" );

		$friend_user = User::get_by_username( $friend_id );

		$starred = boolval( $_POST['starred'] );
		$friend_user->set_starred( $starred );

		wp_send_json_success(
			array(
				'starred' => $friend_user->get_starred(),
			)
		);
	}

	/**
	 * Ajax handler to move a subscription to a folder.
	 */
	public function ajax_move_to_folder() {
		if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['friend_id'] ) ) {
			wp_send_json_error();
			exit;
		}

		check_ajax_referer( 'friends-move-to-folder' );

		$friend_id = sanitize_text_field( wp_unslash( $_POST['friend_id'] ) );
		$folder_id = isset( $_POST['folder_id'] ) ? intval( $_POST['folder_id'] ) : 0;

		$friend_user = User::get_by_username( $friend_id );
		if ( ! $friend_user || is_wp_error( $friend_user ) || ! ( $friend_user instanceof Subscription ) ) {
			wp_send_json_error( 'invalid-user' );
			exit;
		}

		$result = $friend_user->move_to_folder( $folder_id );
		wp_send_json_success( array( 'moved' => $result ) );
	}

	/**
	 * Ajax handler to create a new folder.
	 */
	public function ajax_create_folder() {
		if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['name'] ) ) {
			wp_send_json_error();
			exit;
		}

		check_ajax_referer( 'friends-move-to-folder' );

		$name      = sanitize_text_field( wp_unslash( $_POST['name'] ) );
		$parent_id = isset( $_POST['parent_id'] ) ? intval( $_POST['parent_id'] ) : 0;

		$folder = Subscription::create_folder( $name, $parent_id );
		if ( is_wp_error( $folder ) ) {
			wp_send_json_error( $folder->get_error_message() );
			exit;
		}

		wp_send_json_success(
			array(
				'term_id' => $folder->term_id,
				'name'    => $folder->name,
			)
		);
	}

	/**
	 * Ensure that untrashed friends posts go back to published.
	 *
	 * @param string $new_status      The new status of the post being restored.
	 * @param int    $post_id         The ID of the post being restored.
	 */
	public function untrash_post_status( $new_status, $post_id ) {
		if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
			return $new_status;
		}
		return 'publish';
	}

	/**
	 * Load the template for /friends
	 *
	 * @param  string $template The original template intended to load.
	 * @return string The new template to be loaded.
	 */
	public function template_override( $template ) {
		if ( ! Friends::on_frontend() ) {
			return $template;
		}

		global $args;
		$args = array(
			'friends'     => $this->friends,
			'friend_user' => $this->author,
			'post_format' => $this->post_format,
		);

		if ( $this->template ) {
			global $wp_query;
			$wp_query->is_404 = false;

			status_header( 200 );

			if ( 'block' === $this->theme ) {
				$block_template_content = $this->get_block_template_content_for( $this->template );
				if ( false !== $block_template_content ) {
					global $_wp_current_template_content, $_wp_current_template_id;
					$_wp_current_template_content = $block_template_content;
					$_wp_current_template_id      = get_stylesheet() . '//friends-' . basename( $this->template );
					return ABSPATH . WPINC . '/template-canvas.php';
				}
			}

			if ( 'frontend/index' === $this->template ) {
				$args['frontend_default_view'] = get_user_option( 'friends_frontend_default_view', get_current_user_id() );

			}
			return Friends::template_loader()->get_template_part( $this->template, null, $args, false );
		}

		if ( 'block' === $this->theme ) {
			global $wp_query;
			if ( $wp_query->is_single ) {
				$template_key = 'frontend/single';
				$template_id  = 'friends-single';
			} elseif ( $this->author ) {
				$template_key = 'frontend/author-index';
				$template_id  = 'friends-author-index';
			} else {
				$template_key = 'frontend/index';
				$template_id  = 'friends-index';
			}
			$block_template_content = $this->get_block_template_content_for( $template_key );
			if ( false !== $block_template_content ) {
				global $_wp_current_template_content, $_wp_current_template_id;
				$_wp_current_template_content = $block_template_content;
				$_wp_current_template_id      = get_stylesheet() . '//' . $template_id;
				return ABSPATH . WPINC . '/template-canvas.php';
			}
		}

		$args['frontend_default_view'] = get_user_option( 'friends_frontend_default_view', get_current_user_id() );
		$args['blocks-everywhere']     = false;

		if ( isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['show_welcome'] = true;
		}

		return Friends::template_loader()->get_template_part( 'frontend/index', $this->post_format, $args, false );
	}

	public function get_block_template_content_for( $template_path ) {
		$map = array(
			'frontend/index'         => 'index',
			'frontend/author-index'  => 'friends-author-index',
			'frontend/single'        => 'single-friend_post_cache',
			'frontend/followers'     => 'friends-followers',
			'frontend/add-friend'    => 'friends-add-friend',
			'frontend/subscriptions' => 'friends-subscriptions',
		);
		if ( ! isset( $map[ $template_path ] ) ) {
			return false;
		}
		$template = $map[ $template_path ];
		if ( 'frontend/index' === $template_path && isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$template = 'welcome';
		}
		$file = FRIENDS_PLUGIN_DIR . 'themes/friends/templates/' . $template . '.html';
		if ( ! file_exists( $file ) ) {
			return false;
		}
		return file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
	}

	public function get_static_frontend_template( $path ) {
		global $friends_args;
		global $wp_query;
		$wp_query->is_singular = false;

		switch ( $path ) {
			case 'add-friend':
				$path = 'frontend/add-friend';
				break;

			case 'subscriptions':
				wp_safe_redirect( home_url( '/friends/following/' ) );
				exit;

			case 'following':
				$friends_args = array();
				$path         = 'frontend/subscriptions';
				break;

			case 'messages':
				$friends_args = array(
					'title'            => __( 'Direct Messages', 'friends' ),
					'no-bottom-margin' => true,
				);
				$path         = 'frontend/messages';
				break;

			case 'followers':
				if ( ! class_exists( '\Activitypub\Collection\Followers' ) ) {
					return 'frontend/index';
				}

				$friends_args = array();
				if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
					$friends_args['only_mutual'] = true;
					$friends_args['title'] = __( 'Your Mutual Followers', 'friends' );
				} else {
					$friends_args['only_mutual'] = false;
					$friends_args['title'] = __( 'Your Followers', 'friends' );
				}
				$friends_args['user_id'] = get_current_user_id();
				$path = 'frontend/followers';
				break;

			case 'blog-followers':
				$friends_args = array();
				if ( ! class_exists( '\Activitypub\Collection\Actors' ) ) {
					return 'frontend/index';
				}
				if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
					$friends_args['only_mutual'] = true;
					$friends_args['title'] = __( 'Your Mutual Blog Followers', 'friends' );
				} else {
					$friends_args['only_mutual'] = false;
					$friends_args['title'] = __( 'Your Blog Followers', 'friends' );
				}
				$friends_args['user_id'] = \Activitypub\Collection\Actors::BLOG_USER_ID;
				$path = 'frontend/followers';
				break;
			case 'mutual':
				if ( ! class_exists( '\Activitypub\Collection\Followers' ) ) {
					return 'frontend/index';
				}

				$friends_args            = array();
				$friends_args['title']  = __( 'Friends', 'friends' );
				$friends_args['filter'] = 'following';
				$friends_args['user_id'] = get_current_user_id();
				$path                    = 'frontend/followers';
				break;

			default:
				return 'frontend/index';
		}

		$wp_query->is_404 = false;
		return $path;
	}

	/**
	 * Modify the Friends page title depending on context, for example add an author name or post format.
	 *
	 * @param      string $title  The original title.
	 *
	 * @return     string  The modified title.
	 */
	public function header_widget_title( $title ) {
		$title = '<a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $title ) . '</a>';
		if ( $this->author ) {
			$title .= ' &raquo; <a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
		}
		if ( $this->post_format ) {
			$post_formats = get_post_format_strings();
			$title .= ' &raquo; ' . $post_formats[ $this->post_format ];
		}
		return $title;
	}

	/**
	 * Output a link, potentially augmented with authication information.
	 *
	 * @param      string $url          The url.
	 * @param      string $text             The link text.
	 * @param      array  $html_attributes    HTML attributes.
	 * @param      User   $friend_user  The friend user.
	 */
	public function link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) {
		echo wp_kses(
			self::get_link( $url, $text, $html_attributes, $friend_user ),
			array(
				'a'    => array(
					'href'        => array(),
					'title'       => array(),
					'target'      => array(),
					'rel'         => array(),
					'class'       => array(),
					'style'       => array(),
					'data-nonce'  => array(),
					'data-cnonce' => array(),
					'data-token'  => array(),
					'data-friend' => array(),
					'data-id'     => array(),
					'data-url'    => array(),
				),
				'span' => array( 'class' => array() ),
			)
		);
	}

	/**
	 * Get a link, potentially augmented with authication information.
	 *
	 * @param      string $url              The url.
	 * @param      string $text             The link text.
	 * @param      array  $html_attributes  HTML attributes.
	 * @param      User   $friend_user      The friend user.
	 *
	 * @return     string       The link.
	 */
	public static function get_link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) {
		if ( is_null( $friend_user ) ) {
			$friend_user = new User( get_the_author_meta( 'ID' ) );
		}

		$link = '<a href="' . esc_url( $url ) . '"';
		foreach ( $html_attributes as $name => $value ) {
			if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) {
				continue;
			}
			$link .= ' ' . $name . '="' . esc_attr( $value ) . '"';
		}
		$link .= ' title="' . esc_attr( $text ) . '">';
		if ( isset( $html_attributes['dashicon_front'] ) ) {
			$link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_front'] ) . '"></span>';
		}
		$link .= '<span class="text">' . esc_html( $text ) . '</span>';
		if ( isset( $html_attributes['dashicon_back'] ) ) {
			$link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_back'] ) . '"></span>';
		}
		$link .= '</a>';

		return $link;
	}

	public static function get_widget_open_state( $widget ) {
		static $state = null;
		if ( is_null( $state ) ) {
			$state = get_user_meta( get_current_user_id(), 'friends_widget_state', true );
			if ( ! is_array( $state ) ) {
				$state = array();
			}
		}
		return isset( $state[ $widget ] ) && is_string( $state[ $widget ] ) ? $state[ $widget ] : 'open';
	}

	public function ajax_set_widget_open_state() {
		if ( ! isset( $_POST['widget'] ) || ! isset( $_POST['state'] ) ) {
			wp_send_json_error();
			exit;
		}

		check_ajax_referer( 'friends_widget_state' );

		$widget = sanitize_text_field( wp_unslash( $_POST['widget'] ) );
		$open_state = sanitize_text_field( wp_unslash( $_POST['state'] ) );

		$state = get_user_meta( get_current_user_id(), 'friends_widget_state', true );
		if ( ! is_array( $state ) ) {
			$state = array();
		}
		$state[ $widget ] = $open_state;
		update_user_meta( get_current_user_id(), 'friends_widget_state', $state );

		wp_send_json_success();
	}

	public function ajax_get_post_counts() {
		check_ajax_referer( 'friends_post_counts' );
		$counts = array_fill_keys( get_post_format_slugs(), 0 );

		foreach ( $this->friends->get_post_count_by_post_format( true ) as $post_format => $count ) {
			$counts[ $post_format ] = $this->friends->get_post_format_plural_string( $post_format, $count );
		}

		$post_counts = $this->friends->get_post_count_by_post_status( true );
		if ( isset( $post_counts->trash ) && $post_counts->trash ) {
			$counts['trash'] = sprintf( /* translators: %s is the number of hidden posts */_n( '%s hidden items', '%s hidden items', $post_counts->trash, 'friends' ), number_format_i18n( $post_counts->trash ) );
		} else {
			$counts['trash'] = 0;
		}

		wp_send_json_success( $counts );
	}

	/**
	 * Don't show the edit link for friend posts.
	 *
	 * @param  string $link    The edit link.
	 * @return string|bool The edit link or false.
	 */
	public function friend_post_edit_link( $link ) {
		global $post;

		if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
			if ( Friends::on_frontend() ) {
				$new_link = false;
			} else {
				$new_link = get_the_guid( $post );
			}
			/**
			 * Allow overriding the link for editing friend posts.
			 *
			 * By default on the Frontend, a post cannot be edited because $new_link is false.
			 *
			 * Example:
			 *
			 * ```php
			 * add_filter( 'friend_post_edit_link', function( $link, $original_link ) {
			 *     if ( ! $link ) {
			 *          $link = $original_link; // always allow editing.
			 *      }
			 *      return $link;
			 * }, 10, 2 );
			 * ```
			 */
			return apply_filters( 'friend_post_edit_link', $new_link, $link );
		}
		return $link;
	}

	/**
	 * Link friend posts to the remote site.
	 *
	 * @param string   $post_link The post's permalink.
	 * @param \WP_Post $post      The post in question.
	 * @return string The overriden post link.
	 */
	public function friend_post_link( $post_link, \WP_Post $post ) {
		if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
			return get_the_guid( $post );
		}
		return $post_link;
	}

	/**
	 * Link friend post comments to the local friends page.
	 *
	 * @param string      $link    The comment permalink.
	 * @param \WP_Comment $comment The comment object.
	 * @return string The overriden comment link.
	 */
	public function friend_post_comment_link( $link, \WP_Comment $comment ) {
		// Don't override external ActivityPub comments - they should link to their source.
		if ( 'activitypub' === $comment->comment_type || 'activitypub' === get_comment_meta( $comment->comment_ID, 'protocol', true ) ) {
			return $link;
		}

		$post = get_post( $comment->comment_post_ID );
		if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
			$friend_user = User::get_post_author( $post );
			return $friend_user->get_local_friends_page_url( $post->ID ) . '#comment-' . $comment->comment_ID;
		}
		return $link;
	}

	/**
	 * Potentially override the post author name with metadata.
	 *
	 * @param      string $overridden_author_name  The already overridden author name.
	 * @param      string $author_name  The author name.
	 * @param      int    $post_id      The post id.
	 *
	 * @return     string  The modified author name.
	 */
	public function override_author_name( $overridden_author_name, $author_name, $post_id ) {
		if ( $overridden_author_name && $overridden_author_name !== $author_name ) {
			return $overridden_author_name;
		}
		$override_author_name = get_post_meta( $post_id, 'author', true );
		if ( $override_author_name ) {
			return $override_author_name;
		}
		return $author_name;
	}

	public function expose_opml( $viewable, $pagename ) {
		if ( 'opml' === $pagename ) {
			return true;
		}
		return $viewable;
	}

	/**
	 * Render the Friends OPML
	 *
	 * @param      bool $only_public  Only public feed URLs.
	 */
	protected function render_opml( $only_public = false ) {
		if ( ! \is_user_logged_in() ) {
			$only_public = true;
			$user_id = Friends::get_main_friend_user_id();
			$user = new User( $user_id );
		} else {
			$user = wp_get_current_user();
		}

		// translators: %s is a name.
		$title = sprintf( __( '%s is following', 'friends' ), $user->display_name );
		$filename = 'friends-';
		if ( ! $only_public ) {
			$title = __( 'My Friends', 'friends' );
			$filename .= 'private-';
		}
		$filename .= $user->user_login . '.opml';

		$feeds = array();
		$users = array();

		$friend_users = User_Query::all_associated_users();
		foreach ( $friend_users->get_results() as $friend_user ) {
			$role = $friend_user->get_role_name( true, 9 );
			if ( $only_public ) {
				$role = 'Feeds';
			}
			if ( ! isset( $users[ $role ] ) ) {
				$users[ $role ] = array();
			}

			$users[ $role ][] = $friend_user;
		}
		ksort( $users );
		foreach ( $users as $role => $friend_users ) {
			foreach ( $friend_users as $friend_user ) {
				$user_feeds = $friend_user->get_active_feeds();

				$need_local_feed = false;

				foreach ( $user_feeds as $feed ) {
					if ( $feed->get_parser() === Feed_Parser_SimplePie::SLUG ) {
						break;
					}
					switch ( $feed->get_mime_type() ) {
						case 'application/atom+xml':
						case 'application/atomxml':
						case 'application/rss+xml':
						case 'application/rssxml':
							break;
						default:
							$need_local_feed = true;
							break 2;
					}
				}

				if ( $need_local_feed && ! $only_public ) {
					$user_feeds = array_slice( $user_feeds, 0, 1 );
				}

				$user = array(
					'friend_user' => $friend_user,
					'feeds'       => array(),
				);
				$html_url = $friend_user->user_url;

				foreach ( $user_feeds as $feed ) {
					$type = 'rss';
					$feed_title = $friend_user->display_name;

					if ( ! $only_public ) {
						$html_url = $feed->get_local_html_url();
					}

					if ( $need_local_feed ) {
						if ( $only_public ) {
							// Cannot create a public URL.
							continue;
						}
						$xml_url = $feed->get_local_url();
						// phpcs:disable WordPress.Security.NonceVerification.Recommended
						if ( isset( $_GET['auth'] ) ) {
							$xml_url = add_query_arg( 'auth', sanitize_text_field( wp_unslash( $_GET['auth'] ) ), $xml_url );
							// phpcs:enable WordPress.Security.NonceVerification.Recommended
						}
					} else {
						if ( $only_public ) {
							$xml_url = $feed->get_url();
						} else {
							$xml_url = $feed->get_url();
						}
						if ( 'application/atom+xml' === $feed->get_mime_type() ) {
							$type = 'atom';
						}
					}
					$user['feeds'][] = array(
						'xml_url'  => $xml_url,
						'html_url' => $html_url,
						'title'    => $feed_title,
						'type'     => $type,
					);
				}

				if ( ! empty( $user['feeds'] ) ) {
					if ( ! isset( $feeds[ $role ] ) ) {
						$feeds[ $role ] = array();
					}
					$feeds[ $role ][] = $user;
				}
			}
		}
		Friends::template_loader()->get_template_part(
			'admin/opml',
			null,
			array(
				'title'    => $title,
				'feeds'    => $feeds,
				'filename' => $filename,
			)
		);
		exit;
	}

	private function has_required_priviledges() {
		if ( Friends::is_main_user() ) {
			return true;
		}

		if ( is_multisite() ) {
			if ( is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
				return true;
			}

			if ( is_super_admin( get_current_user_id() ) ) {
				// Super admins would count as administrators.
				return false;
			}
		}

		if ( current_user_can( 'manage_options' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Exclude the compose post format from the main RSS feed.
	 *
	 * @param  \WP_Query $query The main query.
	 * @return \WP_Query The modified main query.
	 */
	public function exclude_compose_format_from_feed( $query ) {
		if ( ! $query->is_main_query() || ! $query->is_feed() ) {
			return $query;
		}

		if ( ! get_option( 'friends_exclude_compose_format_from_feed' ) ) {
			return $query;
		}

		$format = get_option( 'friends_compose_post_format', 'status' );
		if ( ! $format || 'standard' === $format ) {
			return $query;
		}

		$tax_query   = (array) $query->get( 'tax_query' );
		$tax_query[] = array(
			'taxonomy' => 'post_format',
			'field'    => 'slug',
			'terms'    => array( 'post-format-' . $format ),
			'operator' => 'NOT IN',
		);
		$query->set( 'tax_query', $tax_query );

		return $query;
	}

	/**
	 * Modify the main query for the /friends page
	 *
	 * @param  \WP_Query $query The main query.
	 * @return \WP_Query The modified main query.
	 */
	public function friend_posts_query( $query ) {
		global $wp_query, $wp, $authordata;
		if ( $wp_query !== $query || $query->is_admin() || $query->is_home() ) {
			return $query;
		}

		$pagename = '';
		if ( isset( $wp_query->query['pagename'] ) ) {
			$pagename = $wp_query->query['pagename'];
		} elseif ( isset( $wp_query->query['category_name'] ) ) {
			$pagename = $wp_query->query['category_name'];
		} elseif ( isset( $wp_query->query['name'] ) ) {
			$pagename = $wp_query->query['name'];
		}

		$pagename_parts = explode( '/', trim( $pagename, '/' ) );
		$is_friends = array_shift( $pagename_parts );

		if ( 'friends' !== $is_friends ) {
			return $query;
		}

		if ( isset( $pagename_parts[0] ) && 'settings' === $pagename_parts[0] ) {
			wp_safe_redirect( self_admin_url( 'admin.php?page=friends-settings' ) );
			exit;
		}

		// Not available for the general public or friends.
		$viewable = $this->has_required_priviledges() && Friends::on_frontend();
		if ( $query->is_feed() ) {
			// Feeds can be viewed through extra authentication.
			if ( isset( $wp_query->query['pagename'] ) ) {
				if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
					$viewable = true;
				}
			}
		}

		if ( ! $viewable && isset( $wp_query->query['pagename'] ) ) {
			if ( apply_filters( 'friends_friend_posts_query_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
				$viewable = true;
			}
		}

		$page_id = get_query_var( 'page' );
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['share'] ) ) {
			$share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id );
			if ( $_GET['share'] === $share_hash ) {
				$viewable = true;
			}
		}

		if ( ! $viewable ) {
			if ( $query->is_feed() ) {
				status_header( 404 );
				$query->set_404();
			} elseif ( ! Friends::on_frontend() ) {
				if ( count( $pagename_parts ) > 0 ) {
					wp_safe_redirect( home_url( '/friends/' ) );
					exit;
				}
			} elseif ( ! Friends::is_main_user() ) {
				wp_die( esc_html__( 'You are not allowed to view this page.', 'friends' ) );
			}

			return $query;
		}

		// Don't let the Post Kinds plugin interfere with this query.
		remove_action( 'pre_get_posts', array( 'Kind_Taxonomy', 'kind_firehose_query' ), 99 );

		switch_to_locale( get_user_locale() );

		$tax_query = array();
		$post_format = null;

		while ( $pagename_parts ) {
			$pagename_part = $pagename_parts[0];
			$current_part = array_shift( $pagename_parts );
			if ( 'reaction' === substr( $current_part, 0, 8 ) && strlen( $current_part ) > 8 ) {
				$reaction = Reactions::validate_emoji( substr( $current_part, 8 ) );
				if ( ! $reaction ) {
					continue;
				}
				$this->reaction = $reaction;
				if ( ! empty( $tax_query ) ) {
					$tax_query['relation'] = 'AND';
				}

				$tax_query[] = array(
					'taxonomy' => 'friend-reaction-' . get_current_user_id(),
					'field'    => 'slug',
					'terms'    => array( substr( $current_part, 8 ) ),
				);
				continue;
			}

			switch ( $current_part ) {
				case 'opml':
					return $this->render_opml( isset( $_REQUEST['public'] ) && boolval( $_REQUEST['public'] ) );

				case 'type':
					$post_format = array_shift( $pagename_parts );
					$tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) );
					if ( $tax_query ) {
						$this->post_format = $post_format;
					}
					break;

				case 'tag':
					if ( empty( $pagename_parts ) && $page_id ) {
						// Support numeric tags.
						$this->tag = strval( $page_id );
						$page_id = false;
					} else {
						$this->tag = array_shift( $pagename_parts );
					}
					$tax_query = $this->friends->wp_query_get_post_tag_tax_query( $tax_query, $this->tag );
					break;

				default: // Maybe an author.
					$author = User::get_by_username( $current_part );
					if ( false === $author || is_wp_error( $author ) ) {
						if ( $query->is_feed() ) {
							status_header( 404 );
							$query->set_404();
							return $query;
						}

						$template = $this->get_static_frontend_template( $current_part );
						if ( 'frontend/index' !== $template ) {
							$wp_query->is_404 = false;
							$query->is_404 = false;
							status_header( 200 );
							add_filter( 'pre_handle_404', '__return_true' );
							$this->template = $template;
							return $query;
						}
						wp_safe_redirect( home_url( '/friends/' ) );
						exit;
					}

					$this->author = $author;
					break;
			}
		}

		$this->is_friends_page = true;
		$query->is_friends_page = true;
		$query->is_singular = false;
		$query->is_single = false;
		$query->is_category = false;
		$query->is_archive = false;
		$query->queried_object = null;
		$query->queried_object_id = null;
		$post_types = apply_filters( 'friends_frontend_post_types', array() );

		if ( 'status' === $post_format ) {
			// Show your own posts on the status feed.
			$post_types[] = 'post';
		}

		$query->set( 'post_type', $post_types );
		$query->set( 'tax_query', $tax_query );

		if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) {
			$post_status = array( 'publish', 'private', 'future' );
			if ( isset( $_GET['show-hidden'] ) ) {
				$post_status[] = 'trash';
			}
			$query->set( 'post_status', $post_status );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
		$query->is_page = false;
		$query->is_comment_feed = false;
		$query->set( 'pagename', null );
		$query->set( 'category_name', null );
		if ( get_option( 'posts_per_page' ) < 20 ) {
			$query->set( 'posts_per_page', 20 );
			if ( 'collapsed' === get_user_option( 'friends_frontend_default_view', get_current_user_id() ) && 'status' === $post_format ) {
				$query->set( 'posts_per_page', 30 );
			}
		}

		if ( $page_id ) {
			$query->set( 'page_id', $page_id );
			if ( ! $this->author ) {
				$post = get_post( $page_id );

				if ( $post ) {
					$author = User::get_post_author( $post );
					if ( false !== $author ) {
						$this->author = $author;
					}
				}
			}
			$query->is_single = true;
			$query->is_singular = true;
			$wp->set_query_var( 'page', null );
			$wp->set_query_var( 'page_id', $page_id );
		}

		if ( $this->author ) {
			if ( $this->author instanceof User ) {
				$authordata = $this->author; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				$this->author->modify_query_by_author( $query );
				if ( ! $page_id ) {
					$query->is_author = true;
				}
			} else {
				$this->author = null;
			}
		}

		if ( ! $query->is_singular && ! $query->is_author ) {
			// This is the main friends page.
			$hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
			$hide_from_friends_page = array_diff( array_map( 'intval', (array) $hide_from_friends_page ), array( 0 ) );

			if ( $hide_from_friends_page ) {
				$query->set( 'author__not_in', $hide_from_friends_page );
			}
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['order'] ) ) {
			$order = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) );
			if ( in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
				$query->set( 'order', $order );
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		return $query;
	}
}

````
