PluginProbe
Friends / 4.3.2
Friends v4.3.2
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
← All changes | includes/class-blocks.php +1505 -279 2.9.14.3.2 View file →
@@ -17,29 +17,13 @@
17 17 * @package Friends
18 18 * @author Alex Kirk
19 19 */
20 20 class Blocks {
21 - /**
22 - * Contains a reference to the Friends class.
23 - *
24 - * @var Friends
25 - */
26 - private $friends = null;
27 21
28 22 /**
29 - * Whether an excerpt is currently being generated.
30 - *
31 - * @var int
32 - */
33 - private $current_excerpt = null;
34 -
35 - /**
36 23 * Constructor
37 - *
38 - * @param Friends $friends A reference to the Friends object.
39 24 */
40 - public function __construct( Friends $friends ) {
41 - $this->friends = $friends;
25 + public function __construct() {
42 26 $this->register_hooks();
43 27 }
44 28
45 29 /**
@@ -53,17 +37,30 @@
53 37 return;
54 38 }
55 39 add_action( 'admin_enqueue_scripts', array( $this, 'language_data' ) );
56 40 add_filter( 'render_block', array( $this, 'render_friends_block_visibility' ), 10, 2 );
57 - add_filter( 'get_the_excerpt', array( $this, 'current_excerpt_start' ), 9, 2 );
58 - add_filter( 'get_the_excerpt', array( $this, 'current_excerpt_end' ), 11, 2 );
59 - add_filter( 'wp_loaded', array( $this, 'add_block_visibility_attribute' ), 10, 2 );
60 - add_filter( 'template_redirect', array( $this, 'handle_follow_me' ), 10, 2 );
61 - add_action( 'enqueue_block_editor_assets', array( $this, 'register_friends_block_visibility' ) );
41 + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_sidebar_blocks' ) );
62 42 add_action( 'init', array( $this, 'register_blocks' ) );
63 43 }
64 44
65 45 /**
46 + * Safely get block wrapper attributes, returning a fallback when not in a block context.
47 + *
48 + * @param array $extra_attributes Extra attributes to add.
49 + * @return string The wrapper attributes string.
50 + */
51 + private function get_wrapper_attributes( $extra_attributes = array() ) {
52 + if ( function_exists( 'get_block_wrapper_attributes' ) && \WP_Block_Supports::$block_to_render ) {
53 + return get_block_wrapper_attributes( $extra_attributes );
54 + }
55 + $attrs = '';
56 + foreach ( $extra_attributes as $key => $value ) {
57 + $attrs .= ' ' . $key . '="' . esc_attr( $value ) . '"';
58 + }
59 + return $attrs;
60 + }
61 +
62 + /**
66 63 * Register our blocks.
67 64 */
68 65 public function register_blocks() {
69 66 if ( ! function_exists( 'register_block_type_from_metadata' ) ) {
@@ -76,73 +73,1436 @@
76 73 'render_callback' => array( $this, 'render_friends_list_block' ),
77 74 )
78 75 );
79 76
80 - register_block_type_from_metadata(
81 - FRIENDS_PLUGIN_DIR . '/blocks/friend-posts',
77 + register_block_type(
78 + 'friends/subscriptions-query',
82 79 array(
83 - 'render_callback' => array( $this, 'render_friend_posts_block' ),
80 + 'render_callback' => array( $this, 'render_subscriptions_query_block' ),
84 81 )
85 82 );
86 83
87 - register_block_type_from_metadata(
88 - FRIENDS_PLUGIN_DIR . '/blocks/follow-me',
84 + register_block_type(
85 + 'friends/subscription',
89 86 array(
90 - 'render_callback' => array( $this, 'render_follow_me_block' ),
87 + 'uses_context' => array( 'friends/subscription' ),
88 + 'render_callback' => array( $this, 'render_subscription_block' ),
91 89 )
92 90 );
93 91
94 - register_block_type_from_metadata(
95 - FRIENDS_PLUGIN_DIR . '/blocks/message'
92 + register_block_type(
93 + 'friends/followers',
94 + array(
95 + 'render_callback' => array( $this, 'render_followers_block' ),
96 + )
96 97 );
98 +
99 + register_block_type(
100 + 'friends/add-friend-form',
101 + array(
102 + 'render_callback' => array( $this, 'render_add_friend_form_block' ),
103 + )
104 + );
105 +
106 + $list_supports = array(
107 + 'color' => array(
108 + 'background' => true,
109 + 'text' => true,
110 + 'link' => true,
111 + ),
112 + 'typography' => array(
113 + 'fontSize' => true,
114 + 'lineHeight' => true,
115 + ),
116 + 'spacing' => array(
117 + 'padding' => true,
118 + 'margin' => true,
119 + ),
120 + );
121 +
122 + $chip_supports = array(
123 + 'color' => array(
124 + 'background' => true,
125 + 'text' => true,
126 + 'link' => true,
127 + ),
128 + 'typography' => array(
129 + 'fontSize' => true,
130 + ),
131 + 'spacing' => array(
132 + 'padding' => true,
133 + 'margin' => true,
134 + ),
135 + );
136 +
137 + register_block_type(
138 + 'friends/stats',
139 + array(
140 + 'render_callback' => array( $this, 'render_stats_block' ),
141 + 'supports' => $list_supports,
142 + )
143 + );
144 +
145 + register_block_type(
146 + 'friends/refresh',
147 + array(
148 + 'render_callback' => array( $this, 'render_refresh_block' ),
149 + 'supports' => $list_supports,
150 + )
151 + );
152 +
153 + register_block_type(
154 + 'friends/post-formats',
155 + array(
156 + 'render_callback' => array( $this, 'render_post_formats_block' ),
157 + 'supports' => $list_supports,
158 + )
159 + );
160 +
161 + register_block_type(
162 + 'friends/add-subscription',
163 + array(
164 + 'render_callback' => array( $this, 'render_add_subscription_block' ),
165 + 'supports' => $list_supports,
166 + )
167 + );
168 +
169 + register_block_type(
170 + 'friends/search',
171 + array(
172 + 'render_callback' => array( $this, 'render_search_block' ),
173 + )
174 + );
175 +
176 + register_block_type(
177 + 'friends/feed-title',
178 + array(
179 + 'render_callback' => array( $this, 'render_feed_title_block' ),
180 + 'supports' => array(
181 + 'color' => array(
182 + 'background' => true,
183 + 'text' => true,
184 + 'link' => true,
185 + ),
186 + 'typography' => array(
187 + 'fontSize' => true,
188 + 'lineHeight' => true,
189 + ),
190 + ),
191 + )
192 + );
193 +
194 + register_block_type(
195 + 'friends/feed-chips',
196 + array(
197 + 'render_callback' => array( $this, 'render_feed_chips_block' ),
198 + 'supports' => $chip_supports,
199 + )
200 + );
201 +
202 + register_block_type(
203 + 'friends/welcome',
204 + array(
205 + 'attributes' => array(
206 + 'forceWelcome' => array(
207 + 'type' => 'boolean',
208 + 'default' => false,
209 + ),
210 + ),
211 + 'render_callback' => array( $this, 'render_welcome_block' ),
212 + 'supports' => $list_supports,
213 + )
214 + );
215 +
216 + register_block_type(
217 + 'friends/post-content',
218 + array(
219 + 'render_callback' => array( $this, 'render_post_content_block' ),
220 + )
221 + );
222 +
223 + register_block_type(
224 + 'friends/post-permalink',
225 + array(
226 + 'render_callback' => array( $this, 'render_post_permalink_block' ),
227 + 'supports' => $list_supports,
228 + )
229 + );
230 +
231 + register_block_type(
232 + 'friends/post-reblog',
233 + array(
234 + 'render_callback' => array( $this, 'render_post_reblog_block' ),
235 + )
236 + );
237 +
238 + register_block_type(
239 + 'friends/post-boost',
240 + array(
241 + 'render_callback' => array( $this, 'render_post_boost_block' ),
242 + )
243 + );
244 +
245 + register_block_type(
246 + 'friends/post-reactions',
247 + array(
248 + 'render_callback' => array( $this, 'render_post_reactions_block' ),
249 + )
250 + );
251 +
252 + register_block_type(
253 + 'friends/post-comments',
254 + array(
255 + 'render_callback' => array( $this, 'render_post_comments_block' ),
256 + )
257 + );
258 +
259 + register_block_type(
260 + 'friends/author-star',
261 + array(
262 + 'render_callback' => array( $this, 'render_author_star_block' ),
263 + )
264 + );
265 +
266 + register_block_type(
267 + 'friends/author-avatar',
268 + array(
269 + 'render_callback' => array( $this, 'render_author_avatar_block' ),
270 + )
271 + );
272 +
273 + register_block_type(
274 + 'friends/author-name',
275 + array(
276 + 'render_callback' => array( $this, 'render_author_name_block' ),
277 + 'supports' => array(
278 + 'color' => array(
279 + 'background' => true,
280 + 'text' => true,
281 + 'link' => true,
282 + ),
283 + 'typography' => array(
284 + 'fontSize' => true,
285 + 'lineHeight' => true,
286 + ),
287 + ),
288 + )
289 + );
290 +
291 + register_block_type(
292 + 'friends/author-description',
293 + array(
294 + 'render_callback' => array( $this, 'render_author_description_block' ),
295 + 'supports' => $list_supports,
296 + )
297 + );
298 +
299 + register_block_type(
300 + 'friends/author-chips',
301 + array(
302 + 'render_callback' => array( $this, 'render_author_chips_block' ),
303 + 'supports' => $chip_supports,
304 + )
305 + );
97 306 }
98 307
99 308 /**
309 + * Render the friends/subscriptions-query block.
310 + *
311 + * @param array $attributes Block attributes.
312 + * @param string $content Inner block content.
313 + * @param WP_Block $block Block instance.
314 + * @return string The rendered block HTML.
315 + */
316 + public function render_subscriptions_query_block( $attributes, $content, $block ) {
317 + $subscriptions = User_Query::all_subscriptions()->get_results();
318 +
319 + if ( empty( $subscriptions ) ) {
320 + return '<p>' . esc_html__( "You're not following anyone yet.", 'friends' ) . '</p>';
321 + }
322 +
323 + $items = '';
324 + foreach ( $subscriptions as $subscription ) {
325 + $context = array_merge(
326 + $block->context,
327 + array(
328 + 'friends/subscription' => array(
329 + 'ID' => $subscription->ID,
330 + 'display_name' => $subscription->display_name,
331 + 'user_url' => $subscription->user_url,
332 + 'avatar_url' => $subscription->get_avatar_url(),
333 + 'page_url' => $subscription->get_local_friends_page_url(),
334 + ),
335 + )
336 + );
337 +
338 + $inner_content = '';
339 + foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) {
340 + $inner_content .= ( new \WP_Block( $inner_block, $context ) )->render();
341 + }
342 + $items .= '<li>' . $inner_content . '</li>';
343 + }
344 +
345 + return '<ul class="wp-block-friends-subscriptions-query">' . $items . '</ul>';
346 + }
347 +
348 + /**
349 + * Render the friends/add-friend-form block.
350 + *
351 + * @return string The rendered block HTML.
352 + */
353 + public function render_add_friend_form_block() {
354 + ob_start();
355 + Friends::template_loader()->get_template_part( 'frontend/add-friend-form' );
356 + $form = ob_get_clean();
357 +
358 + return '<div' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-add-friend-form' ) ) . '>' . $form . '</div>';
359 + }
360 +
361 + /**
362 + * Render the friends/subscription block.
363 + *
364 + * @param array $attributes Block attributes.
365 + * @param string $content Inner block content.
366 + * @param WP_Block $block Block instance.
367 + * @return string The rendered block HTML.
368 + */
369 + public function render_subscription_block( $attributes, $content, $block ) {
370 + if ( empty( $block->context['friends/subscription'] ) ) {
371 + return '';
372 + }
373 +
374 + $subscription = $block->context['friends/subscription'];
375 +
376 + $out = '<div class="wp-block-friends-subscription">';
377 + $out .= '<a href="' . esc_url( $subscription['page_url'] ) . '">';
378 + if ( $subscription['avatar_url'] ) {
379 + $out .= '<img src="' . esc_url( $subscription['avatar_url'] ) . '" alt="" class="avatar" width="40" height="40" />';
380 + }
381 + $out .= '<span>' . esc_html( $subscription['display_name'] ) . '</span>';
382 + $out .= '</a>';
383 + $out .= '</div>';
384 +
385 + return $out;
386 + }
387 +
388 + /**
389 + * Render the friends/followers block.
390 + *
391 + * @return string The rendered block HTML.
392 + */
393 + public function render_followers_block() {
394 + global $friends_args;
395 +
396 + if ( ! class_exists( '\ActivityPub\Collection\Followers' ) ) {
397 + return '<section class="followers"><p>' . esc_html__( 'The follower list is currently dependent on the ActivityPub plugin.', 'friends' ) . '</p></section>';
398 + }
399 +
400 + $user_id = isset( $friends_args['user_id'] ) ? $friends_args['user_id'] : get_current_user_id();
401 + $blog_followers = class_exists( '\ActivityPub\Collection\Actors' ) && \ActivityPub\Collection\Actors::BLOG_USER_ID === $user_id;
402 +
403 + $filter = isset( $_GET['filter'] ) ? sanitize_key( $_GET['filter'] ) : 'all'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
404 + if ( ! in_array( $filter, array( 'all', 'following', 'not-following' ), true ) ) {
405 + $filter = 'all';
406 + }
407 + // Backwards compatibility with ?mutual.
408 + if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
409 + $filter = 'following';
410 + }
411 +
412 + $sort = isset( $_GET['sort'] ) ? sanitize_key( $_GET['sort'] ) : 'newest'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
413 + if ( ! in_array( $sort, array( 'newest', 'oldest', 'name' ), true ) ) {
414 + $sort = 'newest';
415 + }
416 +
417 + $search_term = isset( $_GET['q'] ) ? trim( wp_unslash( $_GET['q'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
418 +
419 + $followers_list_page = 'users.php?page=activitypub-followers-list';
420 + if ( $blog_followers ) {
421 + $followers_list_page = 'options-general.php?page=activitypub&tab=followers';
422 + }
423 +
424 + $follower_data = \ActivityPub\Collection\Followers::query( $user_id );
425 + $total = $follower_data['total'];
426 +
427 + // Apply search filter first so downstream work operates on the smaller set.
428 + if ( '' !== $search_term ) {
429 + $search_term_lc = function_exists( 'mb_strtolower' ) ? mb_strtolower( $search_term ) : strtolower( $search_term );
430 + $follower_data['followers'] = array_values(
431 + array_filter(
432 + $follower_data['followers'],
433 + function ( $f ) use ( $search_term_lc ) {
434 + $haystack = '';
435 + if ( isset( $f->post_title ) ) {
436 + $haystack .= $f->post_title . ' ';
437 + }
438 + if ( isset( $f->guid ) ) {
439 + $haystack .= $f->guid . ' ';
440 + }
441 + if ( isset( $f->post_content ) ) {
442 + $haystack .= wp_strip_all_tags( $f->post_content );
443 + }
444 + $haystack = function_exists( 'mb_strtolower' ) ? mb_strtolower( $haystack ) : strtolower( $haystack );
445 + return false !== strpos( $haystack, $search_term_lc );
446 + }
447 + )
448 + );
449 + }
450 +
451 + // Classify followers only when a filter is active.
452 + if ( 'all' !== $filter ) {
453 + $following_ids = array();
454 + $not_following_ids = array();
455 + foreach ( $follower_data['followers'] as $follower ) {
456 + $url = $follower->guid;
457 + $is_following = false;
458 + if ( $url ) {
459 + $is_following = User_Feed::get_by_url( $url );
460 + if ( ! $is_following || is_wp_error( $is_following ) ) {
461 + $is_following = User_Feed::get_by_url( str_replace( '@', 'users/', $url ) );
462 + }
463 + }
464 + if ( $is_following && ! is_wp_error( $is_following ) ) {
465 + $following_ids[] = $follower->ID;
466 + } else {
467 + $not_following_ids[] = $follower->ID;
468 + }
469 + }
470 +
471 + if ( 'following' === $filter ) {
472 + $filtered_followers = array_filter(
473 + $follower_data['followers'],
474 + function ( $f ) use ( $following_ids ) {
475 + return in_array( $f->ID, $following_ids, true );
476 + }
477 + );
478 + } else {
479 + $filtered_followers = array_filter(
480 + $follower_data['followers'],
481 + function ( $f ) use ( $not_following_ids ) {
482 + return in_array( $f->ID, $not_following_ids, true );
483 + }
484 + );
485 + }
486 + } else {
487 + $filtered_followers = $follower_data['followers'];
488 + }
489 +
490 + // Sort.
491 + $filtered_followers = array_values( $filtered_followers );
492 + if ( 'oldest' === $sort ) {
493 + usort(
494 + $filtered_followers,
495 + function ( $a, $b ) {
496 + return $a->ID - $b->ID;
497 + }
498 + );
499 + } elseif ( 'name' === $sort ) {
500 + usort(
501 + $filtered_followers,
502 + function ( $a, $b ) {
503 + return strnatcasecmp( $a->post_title, $b->post_title );
504 + }
505 + );
506 + }
507 +
508 + // Paginate.
509 + $filtered_total = count( $filtered_followers );
510 + $followers_per_page = 20;
511 + $current_page = isset( $_GET['fpage'] ) ? max( 1, absint( $_GET['fpage'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
512 + $page_followers = array_slice( $filtered_followers, ( $current_page - 1 ) * $followers_per_page, $followers_per_page );
513 + $display_followers = array();
514 +
515 + foreach ( $page_followers as $follower ) {
516 + if ( $follower instanceof \WP_Post ) {
517 + $follower = \Activitypub\Collection\Remote_Actors::get_actor( $follower );
518 + if ( is_wp_error( $follower ) ) {
519 + continue;
520 + }
521 + }
522 + $data = $follower->to_array();
523 + $data['url'] = \ActivityPub\object_to_uri( $data['url'] );
524 + $data['server'] = wp_parse_url( $data['url'], PHP_URL_HOST );
525 + $data['css_class'] = '';
526 +
527 + $following = User_Feed::get_by_url( $data['url'] );
528 + if ( ! $following || is_wp_error( $following ) ) {
529 + $following = User_Feed::get_by_url( str_replace( '@', 'users/', $data['url'] ) );
530 + }
531 +
532 + if ( $following && ! is_wp_error( $following ) ) {
533 + $data['friend_user'] = $following->get_friend_user();
534 + $data['action_url'] = $following->get_friend_user()->get_local_friends_page_url();
535 + $data['url'] = $following->get_friend_user()->get_local_friends_page_url();
536 + if ( 'all' === $filter ) {
537 + $data['css_class'] = ' already-following';
538 + }
539 + } else {
540 + $data['friend_user'] = false;
541 + $data['action_url'] = add_query_arg( 'url', $data['url'], admin_url( 'admin.php?page=add-friend' ) );
542 + }
543 + $data['remove_action_url'] = add_query_arg( 's', $data['url'], admin_url( $followers_list_page ) );
544 + $display_followers[] = $data;
545 + }
546 +
547 + $base_url = strtok( $_SERVER['REQUEST_URI'], '?' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
548 + $link_args = array();
549 + if ( 'all' !== $filter ) {
550 + $link_args['filter'] = $filter;
551 + }
552 + if ( 'newest' !== $sort ) {
553 + $link_args['sort'] = $sort;
554 + }
555 + if ( '' !== $search_term ) {
556 + $link_args['q'] = $search_term;
557 + }
558 +
559 + ob_start();
560 + ?>
561 + <section class="followers">
562 + <p>
563 + <?php
564 + if ( $blog_followers ) {
565 + echo esc_html(
566 + sprintf(
567 + // translators: %s is the number of followers.
568 + _n( 'You have %s blog follower.', 'You have %s blog followers.', $total, 'friends' ),
569 + $total
570 + )
571 + );
572 + } else {
573 + echo esc_html(
574 + sprintf(
575 + // translators: %s is the number of followers.
576 + _n( 'You have %s follower.', 'You have %s followers.', $total, 'friends' ),
577 + $total
578 + )
579 + );
580 + }
581 +
582 + ?>
583 + <a href="<?php echo esc_url( admin_url( $followers_list_page ) ); ?>">
584 + <?php
585 + if ( $blog_followers ) {
586 + esc_html_e( 'View all blog followers in wp-admin', 'friends' );
587 + } else {
588 + esc_html_e( 'View all followers in wp-admin', 'friends' );
589 + }
590 + ?>
591 + </a>
592 + </p>
593 + <form method="get" action="<?php echo esc_url( $base_url ); ?>" class="friends-search" role="search">
594 + <label class="friends-search-label">
595 + <?php esc_html_e( 'Search:', 'friends' ); ?>
596 + <input type="search" name="q" value="<?php echo esc_attr( $search_term ); ?>" placeholder="<?php esc_attr_e( 'Name, handle, or description', 'friends' ); ?>" class="friends-search-input" />
597 + </label>
598 + <button type="submit" class="button friends-search-submit"><?php esc_html_e( 'Search', 'friends' ); ?></button>
599 + <?php if ( 'all' !== $filter ) : ?>
600 + <input type="hidden" name="filter" value="<?php echo esc_attr( $filter ); ?>" />
601 + <?php endif; ?>
602 + <?php if ( 'newest' !== $sort ) : ?>
603 + <input type="hidden" name="sort" value="<?php echo esc_attr( $sort ); ?>" />
604 + <?php endif; ?>
605 + <?php
606 + if ( '' !== $search_term ) :
607 + $clear_args = $link_args;
608 + unset( $clear_args['q'] );
609 + $clear_url = $clear_args ? add_query_arg( $clear_args, $base_url ) : $base_url;
610 + ?>
611 + <a href="<?php echo esc_url( $clear_url ); ?>" class="friends-search-clear"><?php esc_html_e( 'Clear', 'friends' ); ?></a>
612 + <?php endif; ?>
613 + </form>
614 + <?php if ( '' !== $search_term ) : ?>
615 + <p>
616 + <?php
617 + echo esc_html(
618 + sprintf(
619 + // translators: %1$s is the number of matches, %2$s is the search term.
620 + _n( '%1$s match for "%2$s"', '%1$s matches for "%2$s"', count( $filtered_followers ), 'friends' ),
621 + number_format_i18n( count( $filtered_followers ) ),
622 + $search_term
623 + )
624 + );
625 + ?>
626 + </p>
627 + <?php endif; ?>
628 + <p>
629 + <?php esc_html_e( 'Filter:', 'friends' ); ?>
630 + <?php
631 + $filters = array(
632 + 'all' => __( 'All', 'friends' ),
633 + 'following' => __( 'Following', 'friends' ),
634 + 'not-following' => __( 'Not Following', 'friends' ),
635 + );
636 + $filter_links = array();
637 + foreach ( $filters as $filter_key => $filter_label ) {
638 + $url = $base_url;
639 + $f_args = $link_args;
640 + if ( 'all' !== $filter_key ) {
641 + $f_args['filter'] = $filter_key;
642 + } else {
643 + unset( $f_args['filter'] );
644 + }
645 + if ( $f_args ) {
646 + $url = add_query_arg( $f_args, $url );
647 + }
648 + if ( $filter === $filter_key ) {
649 + $filter_links[] = '<strong>' . esc_html( $filter_label ) . '</strong>';
650 + } else {
651 + $filter_links[] = '<a href="' . esc_url( $url ) . '">' . esc_html( $filter_label ) . '</a>';
652 + }
653 + }
654 + echo wp_kses(
655 + implode( ' | ', $filter_links ),
656 + array(
657 + 'strong' => array(),
658 + 'a' => array( 'href' => array() ),
659 + )
660 + );
661 + ?>
662 +
663 + &nbsp;&nbsp;
664 + <?php esc_html_e( 'Sort:', 'friends' ); ?>
665 + <?php
666 + $sorts = array(
667 + 'newest' => __( 'Newest', 'friends' ),
668 + 'oldest' => __( 'Oldest', 'friends' ),
669 + 'name' => __( 'Name', 'friends' ),
670 + );
671 + $sort_links = array();
672 + foreach ( $sorts as $sort_key => $sort_label ) {
673 + $url = $base_url;
674 + $s_args = $link_args;
675 + if ( 'newest' !== $sort_key ) {
676 + $s_args['sort'] = $sort_key;
677 + } else {
678 + unset( $s_args['sort'] );
679 + }
680 + if ( $s_args ) {
681 + $url = add_query_arg( $s_args, $url );
682 + }
683 + if ( $sort === $sort_key ) {
684 + $sort_links[] = '<strong>' . esc_html( $sort_label ) . '</strong>';
685 + } else {
686 + $sort_links[] = '<a href="' . esc_url( $url ) . '">' . esc_html( $sort_label ) . '</a>';
687 + }
688 + }
689 + echo wp_kses(
690 + implode( ' | ', $sort_links ),
691 + array(
692 + 'strong' => array(),
693 + 'a' => array( 'href' => array() ),
694 + )
695 + );
696 + ?>
697 + </p>
698 + <ul>
699 + <?php foreach ( $display_followers as $follower ) : ?>
700 + <li>
701 + <details data-nonce="<?php echo esc_attr( wp_create_nonce( 'friends-preview' ) ); ?>" data-following="<?php echo esc_attr( $follower['following'] ); ?>" data-followers="<?php echo esc_attr( $follower['followers'] ); ?>" data-id="<?php echo esc_attr( $follower['id'] ); ?>">
702 + <summary>
703 + <a href="<?php echo esc_url( $follower['url'] ); ?>" class="follower<?php echo esc_attr( $follower['css_class'] ); ?>">
704 + <img width="40" height="40" src="<?php echo esc_attr( $follower['icon']['url'] ); ?>" loading="lazy" class="avatar activitypub-avatar" />
705 + <span class="activitypub-actor">
706 + <strong class="activitypub-name"><?php echo esc_html( $follower['name'] ); ?></strong>
707 + (<span class="activitypub-handle">@<?php echo esc_html( $follower['preferredUsername'] . '@' . $follower['server'] ); ?></span>)
708 + </span>
709 + </a>
710 + <span class="since"><?php echo esc_html( $follower['published'] ); ?></span>
711 + <span class="their-followers"></span>
712 + <span class="their-following"></span>
713 + &nbsp;&nbsp;
714 + <?php if ( $follower['friend_user'] ) : ?>
715 + <span class="follower" title="<?php esc_attr_e( 'Already following', 'friends' ); ?>">
716 + <span class="ab-icon dashicons dashicons-businessperson" style="vertical-align: middle;"><span class="ab-icon dashicons dashicons-yes"></span></span>
717 + </span>
718 + <?php else : ?>
719 + <a href="<?php echo esc_url( $follower['action_url'] ); ?>" class="follower follower-add">
720 + <span class="ab-icon dashicons dashicons-businessperson" style="vertical-align: middle;"><span class="ab-icon dashicons dashicons-plus"></span></span>
721 + </a>
722 + <?php endif; ?>
723 + <a href="<?php echo esc_url( $follower['remove_action_url'] ); ?>" class="follower follower-delete" title="<?php esc_attr_e( 'Remove follower', 'friends' ); ?>" data-nonce="<?php echo esc_attr( wp_create_nonce( 'friends-followers' ) ); ?>" data-handle="<?php echo esc_attr( $follower['preferredUsername'] . '@' . $follower['server'] ); ?>" data-id="<?php echo esc_attr( $follower['id'] ); ?>">
724 + <span class="ab-icon dashicons dashicons-admin-users" style="vertical-align: middle;"><span class="ab-icon dashicons dashicons-no"></span></span>
725 + </a>
726 + <p class="description">
727 + <?php
728 + echo wp_kses(
729 + $follower['summary'],
730 + array(
731 + 'a' => array( 'href' => array() ),
732 + )
733 + );
734 + ?>
735 + </p>
736 + </summary>
737 + <p class="loading-posts">
738 + <span><?php esc_html_e( 'Loading posts', 'friends' ); ?></span>
739 + <i class="form-icon loading"></i>
740 + </p>
741 + </details>
742 + </li>
743 + <?php endforeach; ?>
744 + </ul>
745 + <?php
746 + $total_pages = ceil( $filtered_total / $followers_per_page );
747 + if ( $total_pages > 1 ) {
748 + $pagination_args = array(
749 + 'base' => add_query_arg( 'fpage', '%#%' ),
750 + 'format' => '',
751 + 'current' => $current_page,
752 + 'total' => $total_pages,
753 + );
754 + if ( $link_args ) {
755 + $pagination_args['add_args'] = $link_args;
756 + }
757 + echo '<nav class="pagination">';
758 + echo wp_kses_post( paginate_links( $pagination_args ) );
759 + echo '</nav>';
760 + }
761 + ?>
762 + </section>
763 + <?php
764 + return ob_get_clean();
765 + }
766 +
767 + /**
768 + * Render the friends/stats block.
769 + *
770 + * @param array $attributes Block attributes.
771 + * @return string The rendered block HTML.
772 + */
773 + public function render_stats_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
774 + $subscriptions = User_Query::all_subscriptions();
775 + $subscriptions_count = $subscriptions->get_total();
776 +
777 + $out = '<ul ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-stats' ) ) . '>';
778 +
779 + if ( class_exists( '\ActivityPub\Collection\Followers' ) && \defined( 'ACTIVITYPUB_ACTOR_MODE' ) ) {
780 + $activitypub_actor_mode = \get_option( 'activitypub_actor_mode', \ACTIVITYPUB_ACTOR_MODE );
781 + if ( \ACTIVITYPUB_ACTOR_MODE === $activitypub_actor_mode || \ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $activitypub_actor_mode ) {
782 + $mutual_count = Feed_Parser_ActivityPub::count_mutual_followers( get_current_user_id() );
783 + $out .= '<li><a href="' . esc_url( home_url( '/friends/mutual/' ) ) . '">';
784 + $out .= esc_html(
785 + sprintf(
786 + /* translators: %s: number of mutual friends */
787 + _n( '%s Friend', '%s Friends', $mutual_count, 'friends' ),
788 + $mutual_count
789 + )
790 + );
791 + $out .= '</a></li>';
792 +
793 + $follower_count = Feed_Parser_ActivityPub::count_followers( get_current_user_id() );
794 + $out .= '<li><a href="' . esc_url( home_url( '/friends/followers/' ) ) . '">';
795 + $out .= esc_html(
796 + sprintf(
797 + /* translators: %s: number of followers */
798 + _n( '%s Follower', '%s Followers', $follower_count, 'friends' ),
799 + $follower_count
800 + )
801 + );
802 + $out .= '</a></li>';
803 + }
804 + if ( \ACTIVITYPUB_BLOG_MODE === $activitypub_actor_mode || \ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $activitypub_actor_mode ) {
805 + if ( class_exists( '\ActivityPub\Collection\Actors' ) ) {
806 + $blog_follower_count = Feed_Parser_ActivityPub::count_followers( \ActivityPub\Collection\Actors::BLOG_USER_ID );
807 + $out .= '<li><a href="' . esc_url( home_url( '/friends/blog-followers/' ) ) . '">';
808 + $out .= esc_html(
809 + sprintf(
810 + /* translators: %s: number of followers */
811 + _n( '%s Blog Follower', '%s Blog Followers', $blog_follower_count, 'friends' ),
812 + $blog_follower_count
813 + )
814 + );
815 + $out .= '</a></li>';
816 + }
817 + }
818 + }
819 +
820 + $out .= '<li><a href="' . esc_url( home_url( '/friends/following/' ) ) . '">';
821 + $out .= esc_html(
822 + sprintf(
823 + /* translators: %s: number of subscriptions */
824 + _n( '%s Following', '%s Following', $subscriptions_count, 'friends' ),
825 + $subscriptions_count
826 + )
827 + );
828 + $out .= '</a></li>';
829 + $out .= '</ul>';
830 +
831 + return $out;
832 + }
833 +
834 + /**
835 + * Render the friends/refresh block.
836 + *
837 + * @param array $attributes Block attributes.
838 + * @return string The rendered block HTML.
839 + */
840 + public function render_refresh_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
841 + return '<p ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-refresh' ) ) . '><a href="' . esc_url( home_url( '/friends/?refresh' ) ) . '">' . esc_html__( 'Refresh', 'friends' ) . '</a></p>';
842 + }
843 +
844 + /**
845 + * Render the friends/post-formats block.
846 + *
847 + * @param array $attributes Block attributes.
848 + * @return string The rendered block HTML.
849 + */
850 + public function render_post_formats_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
851 + $out = '<ul ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-post-formats' ) ) . '>';
852 + $out .= '<li><a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html_x( 'All', 'all posts', 'friends' ) . '</a></li>';
853 +
854 + $default_formats = array( 'standard', 'status', 'image', 'video' );
855 + foreach ( get_post_format_strings() as $slug => $title ) {
856 + if ( in_array( $slug, $default_formats, true ) ) {
857 + $out .= '<li><a href="' . esc_url( home_url( '/friends/type/' . $slug . '/' ) ) . '">' . esc_html( $title ) . '</a></li>';
858 + }
859 + }
860 +
861 + $out .= '</ul>';
862 + return $out;
863 + }
864 +
865 + /**
866 + * Render the friends/add-subscription block.
867 + *
868 + * @param array $attributes Block attributes.
869 + * @return string The rendered block HTML.
870 + */
871 + public function render_add_subscription_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
872 + $out = '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-add-subscription' ) ) . '>';
873 + $out .= '<a href="' . esc_url( admin_url( 'admin.php?page=add-friend' ) ) . '">';
874 + $out .= esc_html__( 'Follow', 'friends' );
875 + $out .= '</a></div>';
876 + return $out;
877 + }
878 +
879 + /**
880 + * Render the friends/search block.
881 + *
882 + * @return string The rendered block HTML.
883 + */
884 + public function render_search_block() {
885 + $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
886 + $out = '<div class="wp-block-friends-search">';
887 + $out .= '<form action="' . esc_url( home_url( '/friends/' ) ) . '">';
888 + $out .= '<input type="text" name="s" placeholder="' . esc_attr__( 'Search or paste URL', 'friends' ) . '" value="' . esc_attr( $search ) . '" autocomplete="off" data-nonce="' . esc_attr( wp_create_nonce( 'friends-autocomplete' ) ) . '" />';
889 + $out .= ' <button type="submit">' . esc_html__( 'Search', 'friends' ) . '</button>';
890 + $out .= '</form></div>';
891 + return $out;
892 + }
893 +
894 + /**
895 + * Render the friends/feed-header block.
896 + *
897 + * @return string The rendered block HTML.
898 + */
899 + /**
900 + * Render the friends/feed-title block.
901 + *
902 + * @return string The rendered block HTML.
903 + */
904 + public function render_feed_title_block() {
905 + $friends = Friends::get_instance();
906 + $frontend = $friends->frontend;
907 +
908 + // Determine feed title.
909 + $title = __( 'Main Feed', 'friends' );
910 + if ( $frontend->template ) {
911 + $template_titles = array(
912 + 'frontend/add-friend' => __( 'Add Friend', 'friends' ),
913 + 'frontend/followers' => __( 'Followers', 'friends' ),
914 + 'frontend/subscriptions' => __( 'Following', 'friends' ),
915 + );
916 + if ( isset( $template_titles[ $frontend->template ] ) ) {
917 + $title = $template_titles[ $frontend->template ];
918 + }
919 + }
920 +
921 + $format_titles = array(
922 + 'standard' => _x( 'Post feed', 'Post format', 'friends' ),
923 + 'aside' => _x( 'Aside feed', 'Post format', 'friends' ),
924 + 'chat' => _x( 'Chat feed', 'Post format', 'friends' ),
925 + 'gallery' => _x( 'Gallery feed', 'Post format', 'friends' ),
926 + 'link' => _x( 'Link feed', 'Post format', 'friends' ),
927 + 'image' => _x( 'Image feed', 'Post format', 'friends' ),
928 + 'quote' => _x( 'Quote feed', 'Post format', 'friends' ),
929 + 'status' => _x( 'Status feed', 'Post format', 'friends' ),
930 + 'video' => _x( 'Video feed', 'Post format', 'friends' ),
931 + 'audio' => _x( 'Audio feed', 'Post format', 'friends' ),
932 + );
933 + if ( $frontend->post_format && isset( $format_titles[ $frontend->post_format ] ) ) {
934 + $title = $format_titles[ $frontend->post_format ];
935 + }
936 +
937 + if ( isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
938 + $title = sprintf(
939 + // translators: %s is a search term.
940 + __( 'Search for "%s"', 'friends' ),
941 + esc_html( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
942 + );
943 + }
944 +
945 + // Build display title with reaction/tag context.
946 + $display_title = $title;
947 + if ( $frontend->reaction ) {
948 + $display_title = sprintf(
949 + // translators: %1$s is an emoji reaction, %2$s is a type of feed.
950 + __( 'My %1$s reactions on %2$s', 'friends' ),
951 + $frontend->reaction,
952 + $title
953 + );
954 + } elseif ( $frontend->tag ) {
955 + $display_title = sprintf(
956 + // translators: %1$s is a hash tag, %2$s is a type of feed.
957 + _x( '#%1$s on %2$s', '#tag on feed', 'friends' ),
958 + $frontend->tag,
959 + $title
960 + );
961 + }
962 +
963 + return '<h2 ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-feed-title' ) ) . '><a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $display_title ) . '</a></h2>';
964 + }
965 +
966 + /**
967 + * Render the friends/feed-chips block.
968 + *
969 + * @param array $attributes Block attributes.
970 + * @return string The rendered block HTML.
971 + */
972 + public function render_feed_chips_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
973 + $friends = Friends::get_instance();
974 + $data = $friends->get_main_header_data();
975 +
976 + $hidden_post_count = 0;
977 + if ( isset( $data['post_count_by_post_status']->trash ) ) {
978 + $hidden_post_count = $data['post_count_by_post_status']->trash;
979 + }
980 +
981 + $out = '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-feed-chips' ) ) . '>';
982 +
983 + if ( isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
984 + $search_term = sanitize_text_field( wp_unslash( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
985 + $current_order = isset( $_GET['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) : 'DESC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
986 +
987 + // Clear search chip.
988 + $out .= '<a class="chip" href="' . esc_url( remove_query_arg( 's' ) ) . '">';
989 + $out .= esc_html( sprintf( /* translators: %s is the search term */ __( 'Clear search for "%s"', 'friends' ), $search_term ) );
990 + $out .= '</a> ';
991 +
992 + // Order chips.
993 + if ( 'DESC' === $current_order ) {
994 + $out .= '<a class="chip" href="' . esc_url( add_query_arg( 'order', 'ASC' ) ) . '">' . esc_html__( 'Oldest first', 'friends' ) . '</a> ';
995 + } else {
996 + $out .= '<a class="chip active" href="' . esc_url( add_query_arg( 'order', 'ASC' ) ) . '">' . esc_html__( 'Oldest first', 'friends' ) . '</a> ';
997 + $out .= '<a class="chip" href="' . esc_url( remove_query_arg( 'order' ) ) . '">' . esc_html__( 'Newest first', 'friends' ) . '</a> ';
998 + }
999 + } else {
1000 + // Post count chips.
1001 + $nonce = wp_create_nonce( 'friends_post_counts' );
1002 + foreach ( $data['post_count_by_post_format'] as $post_format => $count ) {
1003 + $out .= '<a class="chip post-count-' . esc_attr( $post_format ) . '" data-nonce="' . esc_attr( $nonce ) . '" href="' . esc_url( home_url( '/friends/type/' . $post_format . '/' ) ) . '">' . esc_html( $friends->get_post_format_plural_string( $post_format, $count ) ) . '</a> ';
1004 + }
1005 +
1006 + // Hidden items chip.
1007 + if ( isset( $_GET['show-hidden'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1008 + $out .= '<a class="chip" href="' . esc_url( remove_query_arg( 'show-hidden' ) ) . '">' . esc_html__( 'Hide hidden items', 'friends' ) . '</a> ';
1009 + } elseif ( $hidden_post_count > 0 ) {
1010 + $out .= '<a class="chip" href="' . esc_url( add_query_arg( 'show-hidden', 1 ) ) . '">';
1011 + $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 ) ) );
1012 + $out .= '</a> ';
1013 + }
1014 +
1015 + // Reaction chips.
1016 + if ( class_exists( __NAMESPACE__ . '\Reactions' ) ) {
1017 + foreach ( Reactions::get_available_emojis() as $slug => $reaction ) {
1018 + $out .= '<a class="chip" href="' . esc_url( home_url( '/friends/reaction' . $slug . '/' ) ) . '">';
1019 + $out .= esc_html( sprintf( /* translators: %s is an emoji */ __( 'Reacted with %s', 'friends' ), $reaction->char ) );
1020 + $out .= '</a> ';
1021 + }
1022 + }
1023 + }
1024 +
1025 + $out .= '</div>';
1026 + return $out;
1027 + }
1028 +
1029 + /**
1030 + * Render the friends/welcome block.
1031 + *
1032 + * @param array $attributes Block attributes.
1033 + * @return string The rendered block HTML.
1034 + */
1035 + public function render_welcome_block( $attributes = array() ) {
1036 + $friends = Friends::get_instance();
1037 + $args = array(
1038 + 'friends' => $friends,
1039 + 'post_format' => $friends->frontend->post_format,
1040 + );
1041 +
1042 + ob_start();
1043 + if ( empty( $attributes['forceWelcome'] ) && User_Query::all_associated_users()->get_total() > 0 ) {
1044 + Friends::template_loader()->get_template_part( 'frontend/no-posts', $args['post_format'], $args );
1045 + } else {
1046 + Friends::template_loader()->get_template_part( 'frontend/no-friends', $args['post_format'], $args );
1047 + }
1048 + $content = ob_get_clean();
1049 +
1050 + return '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-welcome friends-empty-state' ) ) . '>' . $content . '</div>';
1051 + }
1052 +
1053 + /**
1054 + * Render the friends/post-content block.
1055 + *
1056 + * Renders friend post content directly without block parsing.
1057 + *
1058 + * @return string The rendered block HTML.
1059 + */
1060 + public function render_post_content_block() {
1061 + global $post;
1062 + if ( ! $post ) {
1063 + return '<div class="wp-block-friends-post-content"><p><em>' . esc_html__( 'Post content will appear here.', 'friends' ) . '</em></p></div>';
1064 + }
1065 +
1066 + $content = get_the_content();
1067 + $content = wp_kses_post( $content );
1068 + $content = wpautop( $content );
1069 + $content .= Link_Preview::render();
1070 +
1071 + return '<div class="wp-block-friends-post-content">' . $content . '</div>';
1072 + }
1073 +
1074 + /**
1075 + * Render the friends/post-permalink block.
1076 + *
1077 + * Shows "X ago on domain.com Y min read" for each post.
1078 + *
1079 + * @param array $attributes Block attributes.
1080 + * @return string The rendered block HTML.
1081 + */
1082 + public function render_post_permalink_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1083 + global $post;
1084 + if ( ! $post ) {
1085 + return '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-post-permalink' ) ) . '><em>' . esc_html__( '3 days ago on example.com', 'friends' ) . ' <span class="reading-time">' . esc_html__( '2 min read', 'friends' ) . '</span></em></div>';
1086 + }
1087 +
1088 + $friend_user = User::get_post_author( $post );
1089 + if ( ! $friend_user || is_wp_error( $friend_user ) ) {
1090 + return '';
1091 + }
1092 +
1093 + $author_url = $friend_user->get_local_friends_page_url();
1094 + $guid = get_the_guid( $post );
1095 + $domain = wp_parse_url( $guid, PHP_URL_HOST );
1096 + $local_url = $author_url . $post->ID . '/';
1097 +
1098 + // Reading time.
1099 + $read_time_seconds = Frontend::calculate_read_time( get_the_content() );
1100 + $read_time = '';
1101 + if ( $read_time_seconds >= 60 ) {
1102 + $mins = ceil( $read_time_seconds / MINUTE_IN_SECONDS );
1103 + /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
1104 + $read_time = sprintf( _n( '%s min', '%s mins', $mins ), $mins ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1105 + } elseif ( $read_time_seconds > 20 ) {
1106 + $read_time = _x( '< 1 min', 'reading time', 'friends' );
1107 + }
1108 +
1109 + /* translators: %s is a time span */
1110 + $time_ago = sprintf( __( '%s ago' ), human_time_diff( get_post_time( 'U', true ) ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1111 + $out = '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-post-permalink' ) ) . '>';
1112 + $out .= sprintf(
1113 + // translators: %1$s is a date or relative time, %2$s is a site name or domain.
1114 + _x( '%1$s on %2$s', 'at-date-on-post', 'friends' ),
1115 + '<a href="' . esc_url( $local_url ) . '" title="' . esc_attr( get_the_time( 'r' ) ) . '">' . esc_html( $time_ago ) . '</a>',
1116 + '<a href="' . esc_url( $guid ) . '" rel="noopener noreferrer" target="_blank">' . esc_html( $domain ) . '</a>'
1117 + );
1118 + if ( $read_time ) {
1119 + $out .= ' <span class="reading-time" title="' . esc_attr__( 'Estimated reading time', 'friends' ) . '">';
1120 + $out .= esc_html( sprintf( /* translators: %s is a timeframe */ __( '%s read', 'friends' ), $read_time ) );
1121 + $out .= '</span>';
1122 + }
1123 + $out .= '</div>';
1124 +
1125 + return $out;
1126 + }
1127 +
1128 + /**
1129 + * Render the friends/post-reblog block.
1130 + *
1131 + * @return string The rendered block HTML.
1132 + */
1133 + public function render_post_reblog_block() {
1134 + global $post;
1135 + if ( ! $post ) {
1136 + return '<span class="wp-block-friends-post-reblog">' . esc_html_x( 'Reblog', 'button', 'friends' ) . '</span>';
1137 + }
1138 +
1139 + $friend_user = User::get_post_author( $post );
1140 + if ( $friend_user && ! is_wp_error( $friend_user ) && get_current_user_id() === $friend_user->ID ) {
1141 + return '';
1142 + }
1143 +
1144 + $post_id = get_the_ID();
1145 + $reblog_nonce = wp_create_nonce( 'friends-reblog' );
1146 + $reblog_status = get_post_meta( $post_id, 'reblogged', true ) ? ' dashicons-saved' : '';
1147 +
1148 + $out = '<span class="wp-block-friends-post-reblog">';
1149 + $out .= '<a tabindex="0" href="#" data-id="' . esc_attr( $post_id ) . '" data-nonce="' . esc_attr( $reblog_nonce ) . '" class="friends-reblog has-icon-right" title="' . esc_attr_x( 'Reblog', 'button', 'friends' ) . '">';
1150 + $out .= '<i class="dashicons dashicons-controls-repeat"></i> <span class="text">' . esc_html_x( 'Reblog', 'button', 'friends' ) . '</span>';
1151 + $out .= '<i class="friends-reblog-status dashicons' . esc_attr( $reblog_status ) . '"></i>';
1152 + $out .= '</a></span>';
1153 +
1154 + return $out;
1155 + }
1156 +
1157 + /**
1158 + * Render the friends/post-boost block.
1159 + *
1160 + * @return string The rendered block HTML.
1161 + */
1162 + public function render_post_boost_block() {
1163 + global $post;
1164 + if ( ! $post ) {
1165 + return '<span class="wp-block-friends-post-boost">' . esc_html_x( 'Boost', 'button', 'friends' ) . '</span>';
1166 + }
1167 +
1168 + if ( ! has_filter( 'friends_boost' ) && ! class_exists( '\Activitypub\Activitypub' ) ) {
1169 + return '';
1170 + }
1171 +
1172 + $friend_user = User::get_post_author( $post );
1173 + if ( $friend_user && ! is_wp_error( $friend_user ) && get_current_user_id() === $friend_user->ID ) {
1174 + return '';
1175 + }
1176 +
1177 + $post_id = get_the_ID();
1178 + $boost_nonce = wp_create_nonce( 'friends-boost' );
1179 + $boost_status = get_post_meta( $post_id, 'boosted', true ) ? ' dashicons-saved' : '';
1180 +
1181 + $out = '<span class="wp-block-friends-post-boost">';
1182 + $out .= '<a tabindex="0" href="#" data-id="' . esc_attr( $post_id ) . '" data-nonce="' . esc_attr( $boost_nonce ) . '" class="friends-boost has-icon-right" title="' . esc_attr_x( 'Boost', 'button', 'friends' ) . '">';
1183 + $out .= '<i class="dashicons dashicons-controls-repeat"></i> <span class="text">' . esc_html_x( 'Boost', 'button', 'friends' ) . '</span>';
1184 + $out .= '<i class="friends-boost-status dashicons' . esc_attr( $boost_status ) . '"></i>';
1185 + $out .= '</a></span>';
1186 +
1187 + return $out;
1188 + }
1189 +
1190 + /**
1191 + * Render the friends/post-reactions block.
1192 + *
1193 + * @return string The rendered block HTML.
1194 + */
1195 + public function render_post_reactions_block() {
1196 + global $post;
1197 + if ( ! $post ) {
1198 + return '<span class="wp-block-friends-post-reactions">⭐ ❤️</span>';
1199 + }
1200 +
1201 + if ( ! class_exists( __NAMESPACE__ . '\Reactions' ) ) {
1202 + return '';
1203 + }
1204 +
1205 + $post_id = get_the_ID();
1206 + $reactions = Reactions::get_post_reactions();
1207 + $reaction_nonce = wp_create_nonce( 'friends-reaction' );
1208 +
1209 + $out = '<span class="wp-block-friends-post-reactions">';
1210 +
1211 + foreach ( $reactions as $slug => $reaction ) {
1212 + $pressed = $reaction->user_reacted ? ' pressed' : '';
1213 + $out .= '<button class="friends-reaction' . esc_attr( $pressed ) . '" data-id="' . esc_attr( $post_id ) . '" data-emoji="' . esc_attr( $slug ) . '" data-nonce="' . esc_attr( $reaction_nonce ) . '" title="' . esc_attr( $reaction->usernames ) . '">';
1214 + $out .= '<span>' . esc_html( $reaction->emoji ) . '</span> ' . esc_html( $reaction->count );
1215 + $out .= '</button> ';
1216 + }
1217 +
1218 + // Render available emojis as inline buttons for reactions not yet used.
1219 + $available = Reactions::get_available_emojis();
1220 + foreach ( $available as $slug => $emoji ) {
1221 + if ( isset( $reactions[ $slug ] ) ) {
1222 + continue;
1223 + }
1224 + $out .= '<button class="friends-reaction-picker" data-id="' . esc_attr( $post_id ) . '" data-emoji="' . esc_attr( $slug ) . '" data-nonce="' . esc_attr( $reaction_nonce ) . '">' . esc_html( $emoji->char ) . '</button> ';
1225 + }
1226 +
1227 + $out .= '</span>';
1228 + return $out;
1229 + }
1230 +
1231 + /**
1232 + * Render the friends/post-comments block.
1233 + *
1234 + * @return string The rendered block HTML.
1235 + */
1236 + public function render_post_comments_block() {
1237 + global $post;
1238 + if ( ! $post ) {
1239 + return '<span class="wp-block-friends-post-comments">💬 ' . esc_html__( 'Comments' ) . '</span>'; // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1240 + }
1241 +
1242 + $post_id = get_the_ID();
1243 + $has_mention = get_post_meta( $post_id, '_has_mention_in_comments', true );
1244 + $comment_icon = $has_mention ? 'format-status' : 'admin-comments';
1245 + $comment_text = $has_mention ? __( 'Comments (You were mentioned)', 'friends' ) : __( 'Comments' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1246 +
1247 + $out = '<div class="wp-block-friends-post-comments">';
1248 + $out .= '<a href="#" class="comments" data-id="' . esc_attr( $post_id ) . '" data-cnonce="' . esc_attr( wp_create_nonce( 'comments-' . $post_id ) ) . '">';
1249 + $out .= '<i class="dashicons dashicons-' . esc_attr( $comment_icon ) . '"></i> <span class="text">' . esc_html( $comment_text ) . '</span>';
1250 + $out .= '</a>';
1251 + $out .= '<div class="comments-content closed"></div>';
1252 + $out .= '</div>';
1253 +
1254 + return $out;
1255 + }
1256 +
1257 + /**
1258 + * Get the current author from the frontend context.
1259 + *
1260 + * @return User|null
1261 + */
1262 + private function get_frontend_author() {
1263 + $friends = Friends::get_instance();
1264 + return $friends->frontend->author;
1265 + }
1266 +
1267 + /**
1268 + * Render the friends/author-star block.
1269 + *
1270 + * @return string The rendered block HTML.
1271 + */
1272 + public function render_author_star_block() {
1273 + $author = $this->get_frontend_author();
1274 + if ( ! $author ) {
1275 + return '<span class="wp-block-friends-author-star">&#9734;</span>';
1276 + }
1277 +
1278 + $starred = $author->is_starred();
1279 + $star_class = $starred ? 'dashicons-star-filled starred' : 'dashicons-star-empty not-starred';
1280 + $star_nonce = wp_create_nonce( 'star-' . $author->user_login );
1281 +
1282 + return '<a href="" class="wp-block-friends-author-star dashicons ' . esc_attr( $star_class ) . '" data-id="' . esc_attr( $author->user_login ) . '" data-nonce="' . esc_attr( $star_nonce ) . '"></a>';
1283 + }
1284 +
1285 + /**
1286 + * Render the friends/author-avatar block.
1287 + *
1288 + * @return string The rendered block HTML.
1289 + */
1290 + public function render_author_avatar_block() {
1291 + $author = $this->get_frontend_author();
1292 + if ( ! $author ) {
1293 + return '<span class="wp-block-friends-author-avatar"></span>';
1294 + }
1295 +
1296 + $avatar_url = $author->get_avatar_url();
1297 + if ( ! $avatar_url ) {
1298 + return '<span class="wp-block-friends-author-avatar"></span>';
1299 + }
1300 +
1301 + return '<img class="wp-block-friends-author-avatar" src="' . esc_url( $avatar_url ) . '" width="36" height="36" />';
1302 + }
1303 +
1304 + /**
1305 + * Render the friends/author-name block.
1306 + *
1307 + * @param array $attributes Block attributes.
1308 + * @return string The rendered block HTML.
1309 + */
1310 + public function render_author_name_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1311 + $author = $this->get_frontend_author();
1312 + if ( ! $author ) {
1313 + return '<h2 ' . $this->get_wrapper_attributes(
1314 + array(
1315 + 'class' => 'wp-block-friends-author-name',
1316 + 'id' => 'page-title',
1317 + )
1318 + ) . '>' . esc_html__( 'Author Name', 'friends' ) . '</h2>';
1319 + }
1320 +
1321 + return '<h2 ' . $this->get_wrapper_attributes(
1322 + array(
1323 + 'class' => 'wp-block-friends-author-name',
1324 + 'id' => 'page-title',
1325 + )
1326 + ) . '>' . wp_kses_post( apply_filters( 'friends_author_display_name_html', esc_html( $author->display_name ), $author->display_name, $author ) ) . '</h2>';
1327 + }
1328 +
1329 + /**
1330 + * Render the friends/author-description block.
1331 + *
1332 + * @param array $attributes Block attributes.
1333 + * @return string The rendered block HTML.
1334 + */
1335 + public function render_author_description_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1336 + $author = $this->get_frontend_author();
1337 + if ( ! $author ) {
1338 + return '<p ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-description' ) ) . '><em>' . esc_html__( 'Author description will appear here.', 'friends' ) . '</em></p>';
1339 + }
1340 + if ( ! $author->description ) {
1341 + return '';
1342 + }
1343 +
1344 + return '<p ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-description' ) ) . '>' . wp_kses( $author->description, array( 'a' => array( 'href' => array() ) ) ) . '</p>';
1345 + }
1346 +
1347 + /**
1348 + * Render the friends/author-chips block.
1349 + *
1350 + * @param array $attributes Block attributes.
1351 + * @return string The rendered block HTML.
1352 + */
1353 + public function render_author_chips_block( $attributes = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1354 + $friends = Friends::get_instance();
1355 + $author = $this->get_frontend_author();
1356 + if ( ! $author ) {
1357 + return '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-chips' ) ) . '><span class="chip">' . esc_html__( 'Following', 'friends' ) . '</span> <span class="chip">example.com</span> <span class="chip">' . esc_html__( 'Edit', 'friends' ) . '</span></div>';
1358 + }
1359 +
1360 + $out = '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-author-chips' ) ) . '>';
1361 +
1362 + // Role chip.
1363 + $out .= '<span class="chip">' . esc_html( $author->get_role_name() ) . '</span> ';
1364 +
1365 + // Folder chip.
1366 + if ( $author instanceof Subscription ) {
1367 + $folder = $author->get_folder();
1368 + if ( $folder ) {
1369 + $out .= '<span class="chip">&#128193; ' . esc_html( $folder->name ) . '</span> ';
1370 + }
1371 + }
1372 +
1373 + // Since chip.
1374 + $out .= '<span class="chip">' . esc_html( sprintf( /* translators: %s is a date */ __( 'Since %s', 'friends' ), date_i18n( __( 'F j, Y' ), strtotime( $author->user_registered ) ) ) ) . '</span> '; // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1375 +
1376 + // User URL chip.
1377 + if ( $author->user_url ) {
1378 + $domain = wp_parse_url( $author->user_url, PHP_URL_HOST );
1379 + if ( $domain ) {
1380 + $domain = preg_replace( '/^www\./', '', $domain );
1381 + $out .= '<a class="chip" href="' . esc_url( $author->user_url ) . '">' . esc_html( $domain ) . '</a> ';
1382 + }
1383 + }
1384 +
1385 + // Post count chips.
1386 + $post_counts = $author->get_post_count_by_post_format();
1387 + foreach ( $post_counts as $post_format => $count ) {
1388 + $out .= '<a class="chip" href="' . esc_url( $author->get_local_friends_page_url() . 'type/' . $post_format . '/' ) . '">' . esc_html( $friends->get_post_format_plural_string( $post_format, $count ) ) . '</a> ';
1389 + }
1390 +
1391 + // Hidden items chip.
1392 + $hidden_post_count = $author->get_post_in_trash_count();
1393 + if ( $hidden_post_count > 0 ) {
1394 + $out .= '<span class="chip">';
1395 + $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 ) ) );
1396 + $out .= '</span> ';
1397 + }
1398 +
1399 + // Reaction chips.
1400 + if ( class_exists( __NAMESPACE__ . '\Reactions' ) ) {
1401 + foreach ( Reactions::get_available_emojis() as $slug => $reaction ) {
1402 + $out .= '<a class="chip" href="' . esc_url( $author->get_local_friends_page_url() . 'reaction' . $slug . '/' ) . '">';
1403 + $out .= esc_html( sprintf( /* translators: %s is an emoji */ __( 'Reacted with %s', 'friends' ), $reaction->char ) );
1404 + $out .= '</a> ';
1405 + }
1406 + }
1407 +
1408 + // Feed count chip.
1409 + $active_feeds = count( $author->get_active_feeds() );
1410 + $total_feeds = count( $author->get_feeds() );
1411 + if ( $active_feeds > 0 ) {
1412 + $out .= '<a class="chip" href="' . esc_url( admin_url( 'admin.php?page=edit-friend-feeds&user=' . $author->user_login ) ) . '">';
1413 + $out .= esc_html( sprintf( /* translators: %s is the number of feeds */ _n( '%s feed', '%s feeds', $active_feeds, 'friends' ), $active_feeds ) );
1414 + if ( $total_feeds > $active_feeds ) {
1415 + $extra = $total_feeds - $active_feeds;
1416 + $out .= '&nbsp;<small>(+' . esc_html( $extra ) . ' ' . esc_html__( 'more', 'friends' ) . ')</small>';
1417 + }
1418 + $out .= '</a> ';
1419 + }
1420 +
1421 + // Folder selector.
1422 + if ( $author instanceof Subscription ) {
1423 + $folders = Subscription::get_folders();
1424 + $current_folder = $author->get_folder();
1425 + $current_id = $current_folder ? $current_folder->term_id : 0;
1426 + $nonce = wp_create_nonce( 'friends-move-to-folder' );
1427 +
1428 + $out .= '<span class="chip friends-folder-selector">';
1429 + $out .= '&#128193; <select class="friends-move-to-folder" data-id="' . esc_attr( $author->user_login ) . '" data-nonce="' . esc_attr( $nonce ) . '">';
1430 + $out .= '<option value="0"' . selected( $current_id, 0, false ) . '>' . esc_html__( 'No folder', 'friends' ) . '</option>';
1431 + foreach ( $folders as $folder ) {
1432 + $out .= '<option value="' . esc_attr( $folder->term_id ) . '"' . selected( $current_id, $folder->term_id, false ) . '>' . esc_html( $folder->name ) . '</option>';
1433 + }
1434 + $out .= '<option value="new">' . esc_html__( '+ New folder', 'friends' ) . '</option>';
1435 + $out .= '</select>';
1436 + $out .= '</span> ';
1437 + }
1438 +
1439 + // Edit chip.
1440 + $out .= '<a class="chip" href="' . esc_url( admin_url( 'admin.php?page=edit-friend&user=' . $author->user_login ) ) . '">' . esc_html__( 'Edit', 'friends' ) . '</a> ';
1441 +
1442 + // Refresh chip.
1443 + $out .= '<a class="chip" href="' . esc_url( wp_nonce_url( admin_url( 'admin.php?page=friends-refresh&user=' . $author->user_login ), 'friends-refresh' ) ) . '">' . esc_html__( 'Refresh', 'friends' ) . '</a> ';
1444 +
1445 + $out .= '</div>';
1446 + return $out;
1447 + }
1448 +
1449 + /**
100 1450 * Render the Friends List block
101 1451 *
102 1452 * @param array $attributes Attributes set by Blocks.
103 1453 * @return string The new block content.
104 1454 */
105 - public function render_friends_list_block( $attributes ) {
1455 + public function render_friends_list_block( $attributes = array() ) {
106 1456 if ( ! isset( $attributes['user_types'] ) ) {
107 - $attributes['user_types'] = 'friends';
1457 + $attributes['user_types'] = 'subscriptions';
108 1458 }
109 - switch ( $attributes['user_types'] ) {
110 - case 'friend_requests':
111 - $friends = User_Query::all_friend_requests();
112 - $no_users = __( "You currently don't have any friend requests.", 'friends' );
113 - break;
114 1459
115 - case 'friends_subscriptions':
116 - $friends = User_Query::all_associated_users();
117 - $no_users = __( "You don't have any friends or subscriptions yet.", 'friends' );
118 - break;
1460 + if ( 'folders' === $attributes['user_types'] ) {
1461 + return $this->render_friends_list_by_folder( $attributes );
1462 + }
119 1463
120 - case 'subscriptions':
121 - $friends = User_Query::all_subscriptions();
122 - $no_users = __( "You don't have any subscriptions yet.", 'friends' );
123 - break;
1464 + if ( ! empty( $attributes['folder'] ) ) {
1465 + $friends = User_Query::subscriptions_in_folder( intval( $attributes['folder'] ) );
1466 + $folder_term = get_term( intval( $attributes['folder'] ), Subscription::TAXONOMY );
1467 + $no_users = '';
1468 + } else {
1469 + $folder_term = null;
1470 + switch ( $attributes['user_types'] ) {
1471 + case 'starred':
1472 + $friends = User_Query::starred_friends_subscriptions();
1473 + $no_users = '';
1474 + break;
1475 + default:
1476 + case 'subscriptions':
1477 + $friends = User_Query::all_subscriptions();
1478 + $no_users = __( "You're not following anyone yet.", 'friends' );
1479 + break;
1480 + }
1481 + }
124 1482
125 - case 'friends':
126 - default:
127 - $friends = User_Query::all_friends();
128 - $no_users = __( "You don't have any friends yet.", 'friends' );
1483 + if ( $friends->get_total() === 0 ) {
1484 + if ( ! $no_users ) {
1485 + return '';
1486 + }
1487 + return '<span ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list no-users' ) ) . '>' . $no_users . '</span>';
129 1488 }
130 1489
131 - if ( $friends->get_total() === 0 ) {
132 - return '<span class="wp-block-friends-friends-list no-users">' . $no_users . '</span>';
1490 + $heading = '';
1491 + if ( $folder_term && ! is_wp_error( $folder_term ) ) {
1492 + $heading = '<h3>&#128193; ' . esc_html( $folder_term->name ) . '</h3>';
133 1493 }
134 1494
135 - if ( $attributes['users_inline'] ) {
136 - $out = '';
1495 + if ( ! empty( $attributes['users_inline'] ) ) {
1496 + $out = $heading;
137 1497 $first = true;
138 1498 } else {
139 - $out = '<ul>';
1499 + $out = $heading . '<ul ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list' ) ) . '>';
140 1500 }
141 1501 $count = 0;
142 1502 foreach ( $friends->get_results() as $friend_user ) {
143 - $count += 1;
144 - if ( $attributes['users_inline'] ) {
1503 + ++$count;
1504 + if ( ! empty( $attributes['users_inline'] ) ) {
145 1505 if ( ! $first ) {
146 1506 $out .= ', ';
147 1507 }
148 1508 $first = false;
@@ -149,9 +1509,9 @@
149 1509 } else {
150 1510 $out .= '<li>';
151 1511 }
152 1512
153 - if ( friends::has_required_privileges() ) {
1513 + if ( Friends::has_required_privileges() ) {
154 1514 $url = $friend_user->get_local_friends_page_url();
155 1515 } else {
156 1516 $url = $friend_user->user_url;
157 1517 }
@@ -156,17 +1516,17 @@
156 1516 $url = $friend_user->user_url;
157 1517 }
158 1518
159 1519 $out .= sprintf(
160 - '<a class="wp-block-friends-friends-list wp-user" href="%1$s">%2$s</a></li>',
1520 + '<a class="wp-user" href="%1$s">%2$s</a>',
161 1521 esc_url( $url ),
162 1522 esc_html( $friend_user->display_name )
163 1523 );
164 - if ( ! $attributes['users_inline'] ) {
1524 + if ( empty( $attributes['users_inline'] ) ) {
165 1525 $out .= '</li>';
166 1526 }
167 1527 }
168 - if ( ! $attributes['users_inline'] ) {
1528 + if ( empty( $attributes['users_inline'] ) ) {
169 1529 $out .= '</ul>';
170 1530 }
171 1531
172 1532 return $out;
@@ -172,14 +1532,70 @@
172 1532 return $out;
173 1533 }
174 1534
175 1535 /**
1536 + * Render the friends list grouped by folder hierarchy.
1537 + *
1538 + * @param array $attributes Block attributes.
1539 + * @return string The rendered block HTML.
1540 + */
1541 + private function render_friends_list_by_folder( $attributes ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
1542 + $all = User_Query::all_subscriptions();
1543 + $folders = Subscription::get_folders();
1544 +
1545 + if ( $all->get_total() === 0 ) {
1546 + return '<span ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list no-users' ) ) . '>' . esc_html__( "You're not following anyone yet.", 'friends' ) . '</span>';
1547 + }
1548 +
1549 + $out = '<div ' . $this->get_wrapper_attributes( array( 'class' => 'wp-block-friends-friends-list folders' ) ) . '>';
1550 +
1551 + // Render each folder as a collapsible details element.
1552 + foreach ( $folders as $folder ) {
1553 + $folder_subs = User_Query::subscriptions_in_folder( $folder->term_id );
1554 + if ( $folder_subs->get_total() === 0 ) {
1555 + continue;
1556 + }
1557 +
1558 + $out .= '<details class="friends-folder" open>';
1559 + $out .= '<summary>&#128193; ' . esc_html( $folder->name ) . ' <small>(' . $folder_subs->get_total() . ')</small></summary>';
1560 + $out .= '<ul>';
1561 + foreach ( $folder_subs->get_results() as $friend_user ) {
1562 + $url = Friends::has_required_privileges() ? $friend_user->get_local_friends_page_url() : $friend_user->user_url;
1563 + $out .= '<li><a class="wp-user" href="' . esc_url( $url ) . '">' . esc_html( $friend_user->display_name ? $friend_user->display_name : $friend_user->user_login ) . '</a></li>';
1564 + }
1565 + $out .= '</ul>';
1566 + $out .= '</details>';
1567 + }
1568 +
1569 + // Render unfoldered subscriptions.
1570 + $unfoldered = User_Query::unfoldered_subscriptions();
1571 + if ( $unfoldered->get_total() > 0 ) {
1572 + if ( ! empty( $folders ) ) {
1573 + $out .= '<details class="friends-folder" open>';
1574 + $out .= '<summary>' . esc_html__( 'Uncategorized', 'friends' ) . ' <small>(' . $unfoldered->get_total() . ')</small></summary>';
1575 + }
1576 + $out .= '<ul>';
1577 + foreach ( $unfoldered->get_results() as $friend_user ) {
1578 + $url = Friends::has_required_privileges() ? $friend_user->get_local_friends_page_url() : $friend_user->user_url;
1579 + $out .= '<li><a class="wp-user" href="' . esc_url( $url ) . '">' . esc_html( $friend_user->display_name ? $friend_user->display_name : $friend_user->user_login ) . '</a></li>';
1580 + }
1581 + $out .= '</ul>';
1582 + if ( ! empty( $folders ) ) {
1583 + $out .= '</details>';
1584 + }
1585 + }
1586 +
1587 + $out .= '</div>';
1588 + return $out;
1589 + }
1590 +
1591 + /**
176 1592 * Render the Friend Posts block
177 1593 *
178 1594 * @param array $attributes Attributes set by Blocks.
179 1595 * @return string The new block content.
180 1596 */
181 - public function render_friend_posts_block( $attributes ) {
1597 + public function render_friend_posts_block( $attributes = array() ) {
182 1598 $date_formats = array(
183 1599 'Y m d H' => 'human',
184 1600 'Y m d' => 'H:i',
185 1601 'Y w' => 'D j, H:i',
@@ -228,9 +1644,9 @@
228 1644
229 1645 if ( $remaining <= 0 ) {
230 1646 break 2;
231 1647 }
232 - $remaining -= 1;
1648 + --$remaining;
233 1649
234 1650 $author = $friend_user->display_name;
235 1651 if ( $attributes['author_name'] || $attributes['author_avatar'] || $attributes['author_inline'] ) {
236 1652 if ( $attributes['author_inline'] || $last_author !== $author ) {
@@ -296,243 +1712,53 @@
296 1712 return $out;
297 1713 }
298 1714
299 1715 /**
300 - * Register the Block Visibility script.
1716 + * Enqueue the sidebar blocks editor script.
301 1717 */
302 - public function register_friends_block_visibility() {
1718 + public function enqueue_sidebar_blocks() {
303 1719 wp_enqueue_script(
304 - 'friends-block-visibility',
305 - plugins_url( 'blocks/block-visibility/build/index.js', FRIENDS_PLUGIN_FILE ),
306 - array( 'wp-blocks', 'wp-element', 'wp-i18n', 'wp-editor' ),
307 - Friends::VERSION
1720 + 'friends-sidebar-blocks',
1721 + plugins_url( 'blocks/sidebar-blocks/index.js', FRIENDS_PLUGIN_FILE ),
1722 + array( 'wp-blocks', 'wp-element', 'wp-server-side-render' ),
1723 + Friends::VERSION,
1724 + true
308 1725 );
309 1726
310 - wp_enqueue_style(
311 - 'friends-blocks',
312 - plugins_url( 'friends-blocks.css', FRIENDS_PLUGIN_FILE ),
313 - array(),
314 - Friends::VERSION
315 - );
316 - }
317 -
318 - /**
319 - * Adds a block visibility attribute.
320 - */
321 - public function add_block_visibility_attribute() {
322 - $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
323 -
324 - foreach ( $registered_blocks as $block ) {
325 - $block->attributes['friendsVisibility'] = array(
326 - 'type' => 'string',
327 - 'default' => '',
1727 + // Pass folder data to the editor for the friends-list block.
1728 + $folders = Subscription::get_folders();
1729 + $folder_data = array();
1730 + foreach ( $folders as $folder ) {
1731 + $folder_data[] = array(
1732 + 'term_id' => $folder->term_id,
1733 + 'name' => $folder->name,
328 1734 );
329 1735 }
1736 + wp_localize_script( 'friends-sidebar-blocks', 'friendsFolders', $folder_data );
330 1737 }
331 1738
332 1739 /**
333 - * Handle the follow me button click.
334 - */
335 - public function handle_follow_me() {
336 - if ( ! isset( $_REQUEST['friends_friend_request_url'] ) ) {
337 - return;
338 - }
339 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'friends_follow_me' ) ) {
340 - wp_safe_redirect( add_query_arg( 'error', __( 'Unable to verify nonce, please try again.', 'friends' ) ) );
341 - exit;
342 - }
343 - $access_transient_key = 'friends_follow_me_' . crc32( $_SERVER['REMOTE_ADDR'] );
344 - $access_count = get_transient( $access_transient_key );
345 - if ( $access_count >= 10 ) {
346 - header( 'HTTP/1.0 529 Too Many Requests' );
347 - wp_die( 'Too Many Requests' );
348 - }
349 - set_transient( $access_transient_key, $access_count + 1, 3600 );
350 -
351 - $url = $_REQUEST['friends_friend_request_url'];
352 -
353 - $fqdn_regex = '(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)';
354 - if ( false === strpos( $url, 'https://' ) ) {
355 - $url = 'https://' . $url;
356 - }
357 - $parts = wp_parse_url( $url );
358 -
359 - if ( ! preg_match( '/' . $fqdn_regex . '/', $parts['host'] ) ) {
360 - wp_safe_redirect( add_query_arg( 'error', __( 'You specified an invalid URL.', 'friends' ) ) );
361 - exit;
362 - }
363 -
364 - $response = wp_safe_remote_head(
365 - $url,
366 - array(
367 - 'timeout' => 5,
368 - 'redirection' => 5,
369 - 'headers' => array(
370 - 'Accept: text/html',
371 - ),
372 - )
373 - );
374 - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
375 - wp_safe_redirect(
376 - add_query_arg(
377 - 'error',
378 - sprintf( /* translators: %s is an HTTP status code */
379 - __( 'The server returned an HTTP status: %s', 'friends' ),
380 - wp_remote_retrieve_response_code( $response )
381 - )
382 - )
383 - );
384 - exit;
385 - }
386 -
387 - $links = (array) wp_remote_retrieve_header( $response, 'link' );
388 - if ( ! empty( $links ) ) {
389 - $friends_base_url_endpoints = array_filter(
390 - $links,
391 - function ( $link ) {
392 - return preg_match( '/rel="friends-base-url"/', $link );
393 - }
394 - );
395 -
396 - if ( ! empty( $friends_base_url_endpoints ) ) {
397 - header( 'Location: ' . $url . '?add-friend=' . urlencode( home_url() ) );
398 - exit;
399 - }
400 - }
401 -
402 - // Try again for servers that filter out the Link headers.
403 - $response = wp_safe_remote_get(
404 - $url,
405 - array(
406 - 'timeout' => 5,
407 - 'redirection' => 5,
408 - )
409 - );
410 - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
411 - wp_safe_redirect(
412 - add_query_arg(
413 - 'error',
414 - sprintf( /* translators: %s is an HTTP status code */
415 - __( 'The server returned an HTTP status: %s', 'friends' ),
416 - wp_remote_retrieve_response_code( $response )
417 - )
418 - )
419 - );
420 - exit;
421 - }
422 -
423 - $content = wp_remote_retrieve_body( $response );
424 - $mf = Mf2\parse( $content, $url );
425 -
426 - if ( isset( $mf['rel-urls'] ) ) {
427 - foreach ( $mf['rel-urls'] as $link_url => $link ) {
428 - if ( in_array( 'friends-base-url', $link['rels'] ) ) {
429 - header( 'Location: ' . $link_url . '?add-friend=' . urlencode( home_url() ) );
430 - exit;
431 - }
432 - }
433 - }
434 -
435 - header( 'Location: https://wpfriends.at/follow-me?url=' . urlencode( $_REQUEST['friends_friend_request_url'] ) );
436 - exit;
437 - }
438 -
439 - /**
440 - * Add a CSRF to the Follow Me block.
1740 + * Hide blocks that were marked as "only visible for friends".
441 1741 *
442 - * @param array $attributes Block attributes.
443 - * @param string $content Block default content.
444 - * @param \WP_Block $block Block instance.
445 - * @return string The rendered content.
446 - */
447 - public function render_follow_me_block( $attributes, $content, $block ) {
448 - $input = '<input type="text" name="friends_friend_request_url"';
449 - $nonce = wp_nonce_field( 'friends_follow_me', '_wpnonce', true, false );
450 - return str_replace( $input, $nonce . $input, $content );
451 - }
452 -
453 - /**
454 - * Render the "Only visible for friends" Blocks block
1742 + * Since the friendship feature was removed, "only-friends" content
1743 + * stays hidden to prevent accidental exposure. "not-friends" content
1744 + * is shown to everyone.
455 1745 *
456 - * @param string $content The content provided by the user.
457 - * @param object $block Attributes for the block.
458 - * @return string The rendered content.
1746 + * @param string $content The content provided by the user.
1747 + * @param array $block Attributes for the block.
1748 + * @return string The rendered content.
459 1749 */
460 1750 public function render_friends_block_visibility( $content, $block ) {
461 - $css_class = ( empty( $block['attrs'] ) || empty( $block['attrs']['className'] ) ) ? 'default' : $block['attrs']['className'];
462 - $visibility = '';
1751 + $css_class = ( empty( $block['attrs'] ) || empty( $block['attrs']['className'] ) ) ? '' : $block['attrs']['className'];
1752 +
463 1753 if ( preg_match( '/\bonly-friends\b/', $css_class ) ) {
464 - $visibility = 'only-friends';
465 - } elseif ( preg_match( '/\bnot-friends\b/', $css_class ) ) {
466 - $visibility = 'not-friends';
467 - }
468 -
469 - if ( ! $visibility ) {
470 - return $content;
471 - }
472 - $class_only_friends = ' class="only-friends" style="background-color: #efe; padding-left: .5em;"';
473 - $class_not_friends = ' class="not-friends" style="background-color: #fee; padding-left: .5em;"';
474 - $class_watermark = ' class="watermark" style="float: right; padding-top: .5em; padding-right: .5em; font-size: 80%; color: #ccc;"';
475 -
476 - switch ( $visibility ) {
477 - case 'only-friends':
478 - if ( friends::has_required_privileges() ) {
479 - if ( $this->current_excerpt ) {
480 - return $content;
481 - }
482 - return '<div' . $class_only_friends . '><span' . $class_watermark . '>' . __( 'Only friends', 'friends' ) . '</span>' . $content . '</div>';
483 - }
484 - if ( current_user_can( 'friend' ) ) {
485 - return $content;
486 - }
1754 + // Content meant only for friends stays hidden since friendships were removed.
1755 + if ( ! Friends::has_required_privileges() ) {
487 1756 return '';
488 -
489 - case 'not-friends':
490 - if ( friends::has_required_privileges() ) {
491 - if ( $this->current_excerpt ) {
492 - return $content;
493 - }
494 -
495 - return '<div' . $class_not_friends . '><span' . $class_watermark . '>' . __( 'Not friends', 'friends' ) . '</span>' . $content . '</span></div>';
496 - }
497 - if ( current_user_can( 'friend' ) ) {
498 - return '';
499 - }
500 - return $content;
501 -
1757 + }
502 1758 }
503 1759
504 1760 return $content;
505 - }
506 -
507 - /**
508 - * Remember the current post being excerpted. With this we can change the visibility rendering.
509 - *
510 - * @param string $text The text.
511 - * @param \WP_Post $post The post.
512 - *
513 - * @return string The text.
514 - */
515 - public function current_excerpt_start( $text = '', $post = null ) {
516 - if ( $post ) {
517 - $this->current_excerpt = $post->ID;
518 - }
519 - return $text;
520 - }
521 -
522 - /**
523 - * Stop remembering the current post being excerpted.
524 - *
525 - * @param string $text The text.
526 - * @param \WP_Post $post The post.
527 - *
528 - * @return string The text.
529 - */
530 - public function current_excerpt_end( $text = '', $post = null ) {
531 - if ( $post && $this->current_excerpt === $post->ID ) {
532 - $this->current_excerpt = null;
533 - }
534 - return $text;
535 1761 }
536 1762
537 1763 /**
538 1764 * Load up the language data for the Blocks blocks