PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / build / reactions / render.php

render.php in ActivityPub 8.0.2, at build/reactions/render.php

390 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server-side rendering of the `activitypub/reactions` block.
4 *
5 * @package ActivityPub
6 */
7
8 use Activitypub\Blocks;
9 use Activitypub\Comment;
10
11 use function Activitypub\get_post_id;
12 use function Activitypub\is_activitypub_request;
13
14 if ( is_activitypub_request() || is_feed() ) {
15 return;
16 }
17
18 // Get the default display style based on WordPress avatar settings.
19 $default_display_style = get_option( 'show_avatars', true ) ? 'facepile' : 'compact';
20
21 /* @var array $attributes Block attributes. */
22 $attributes = wp_parse_args(
23 $attributes,
24 array(
25 'align' => null,
26 'displayStyle' => $default_display_style,
27 'showActions' => false,
28 )
29 );
30
31 /* @var \WP_Block $block Current block. */
32 $block = $block ?? '';
33
34 /* @var string $content Block content. */
35 $content = $content ?? '';
36
37 if ( empty( $content ) ) {
38 // Fallback for v1.0.0 blocks.
39 $_title = $attributes['title'] ?? __( 'Fediverse Reactions', 'activitypub' );
40 $content = '<h6 class="wp-block-heading">' . esc_html( $_title ) . '</h6>';
41 unset( $attributes['title'] );
42 } else {
43 $content = implode( PHP_EOL, wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) );
44 // Hide empty headings.
45 if ( empty( wp_strip_all_tags( $content ) ) ) {
46 $content = '';
47 }
48 }
49
50 // Get the Post ID from attributes or use the current post.
51 $_post_id = $attributes['postId'] ?? get_the_ID();
52
53 // Generate a unique ID for the block.
54 $block_id = 'activitypub-reactions-block-' . wp_unique_id();
55
56 /*
57 * Determine display style - compact style hides avatars.
58 * For auto-hooked blocks without explicit style, use avatar setting to determine style.
59 */
60 $has_style_class = isset( $attributes['className'] ) && strpos( $attributes['className'], 'is-style-' ) !== false;
61 if ( ! $has_style_class ) {
62 $attributes['className'] = trim( ( $attributes['className'] ?? '' ) . ' is-style-' . $default_display_style );
63 $attributes['displayStyle'] = $default_display_style;
64 }
65
66 $show_avatars = 'facepile' === $attributes['displayStyle'];
67
68 // Fetch reactions.
69 $reactions = array();
70
71 foreach ( Comment::get_comment_types() as $_type => $type_object ) {
72 $_comments = get_comments(
73 array(
74 'post_id' => $_post_id,
75 'type' => $_type,
76 'status' => 'approve',
77 'parent' => 0,
78 )
79 );
80
81 if ( empty( $_comments ) ) {
82 continue;
83 }
84
85 $count = count( $_comments );
86 // phpcs:disable WordPress.WP.I18n
87 $label = sprintf(
88 _n(
89 $type_object['count_single'],
90 $type_object['count_plural'],
91 $count,
92 'activitypub'
93 ),
94 number_format_i18n( $count )
95 );
96 // phpcs:enable WordPress.WP.I18n
97
98 $reactions[ $_type ] = array(
99 'label' => $label,
100 'count' => $count,
101 'items' => array_map(
102 static function ( $comment ) {
103 return array(
104 'name' => html_entity_decode( $comment->comment_author ),
105 'url' => $comment->comment_author_url,
106 'avatar' => get_avatar_url( $comment ),
107 );
108 },
109 $_comments
110 ),
111 );
112 }
113
114 if ( empty( $reactions ) ) {
115 echo '<!-- Reactions block: No reactions found. -->';
116 return;
117 }
118
119 // Set up the Interactivity API config.
120 $config = array(
121 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
122 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
123 );
124
125 if ( $attributes['showActions'] ) {
126 $config['i18n'] = array(
127 'copied' => __( 'Copied!', 'activitypub' ),
128 'copy' => __( 'Copy', 'activitypub' ),
129 'emptyProfileError' => __( 'Please enter a profile URL or handle.', 'activitypub' ),
130 'genericError' => __( 'An error occurred. Please try again.', 'activitypub' ),
131 'intentLabelLike' => __( 'Like this post', 'activitypub' ),
132 'intentLabelAnnounce' => __( 'Boost this post', 'activitypub' ),
133 'invalidProfileError' => __( 'Please enter a valid profile URL or handle.', 'activitypub' ),
134 );
135 }
136
137 wp_interactivity_config( 'activitypub/reactions', $config );
138
139 // Set up the Interactivity API state.
140 wp_interactivity_state( 'activitypub/reactions', array( 'reactions' => array( $_post_id => $reactions ) ) );
141
142 // Render a subset of the most recent reactions for facepile.
143 $reactions = array_map(
144 static function ( $reaction ) use ( $attributes ) {
145 $count = 20;
146 if ( 'wide' === $attributes['align'] ) {
147 $count = 40;
148 } elseif ( 'full' === $attributes['align'] ) {
149 $count = 60;
150 }
151
152 $reaction['items'] = array_slice( array_reverse( $reaction['items'] ), 0, $count );
153
154 return $reaction;
155 },
156 $reactions
157 );
158
159 // Initialize the context for the block.
160 $context = array(
161 'blockId' => $block_id,
162 'modal' => array(
163 'isCompact' => true,
164 'isOpen' => false,
165 'items' => array(),
166 'title' => '',
167 ),
168 'postId' => $_post_id,
169 'reactions' => $reactions,
170 );
171
172 if ( $attributes['showActions'] ) {
173 $context['modal']['intent'] = '';
174 $context['copyButtonText'] = __( 'Copy', 'activitypub' );
175 $context['errorMessage'] = '';
176 $context['isError'] = false;
177 $context['isLoading'] = false;
178 $context['postUrl'] = get_post_id( $_post_id );
179 $context['remoteProfile'] = '';
180 $context['shouldSaveProfile'] = true;
181 }
182
183 // Map comment types to remote intent types.
184 if ( $attributes['showActions'] ) {
185 $intent_map = array(
186 'like' => 'like',
187 'repost' => 'announce',
188 'quote' => 'announce',
189 );
190 }
191
192 // Build reactions content.
193 ob_start();
194 ?>
195 <div class="activitypub-reactions">
196 <?php
197 foreach ( $reactions as $_type => $reaction ) :
198 /* translators: %s: reaction type. */
199 $aria_label = sprintf( __( 'View all %s', 'activitypub' ), Comment::get_comment_type_attr( $_type, 'label' ) );
200 $intent = isset( $intent_map[ $_type ] ) ? $intent_map[ $_type ] : '';
201 ?>
202 <div class="reaction-group" data-reaction-type="<?php echo esc_attr( $_type ); ?>">
203 <?php if ( $attributes['showActions'] && $intent ) : ?>
204 <button
205 class="reaction-action-button has-text-color has-background"
206 data-intent="<?php echo esc_attr( $intent ); ?>"
207 data-wp-on--click="actions.openIntentModal"
208 type="button"
209 aria-label="<?php echo esc_attr( Comment::get_comment_type_attr( $_type, 'singular' ) ); ?>"
210 >
211 <?php echo esc_html( Comment::get_comment_type_attr( $_type, 'singular' ) ); ?>
212 </button>
213 <?php endif; ?>
214 <?php if ( $show_avatars ) : ?>
215 <ul class="reaction-avatars">
216 <template data-wp-each="context.reactions.<?php echo esc_attr( $_type ); ?>.items">
217 <li>
218 <a
219 data-wp-bind--href="context.item.url"
220 data-wp-bind--title="context.item.name"
221 target="_blank"
222 rel="noopener noreferrer"
223 >
224 <img
225 data-wp-bind--src="context.item.avatar"
226 data-wp-bind--alt="context.item.name"
227 data-wp-on--error="callbacks.setDefaultAvatar"
228 class="reaction-avatar"
229 height="32"
230 width="32"
231 src=""
232 alt=""
233 />
234 </a>
235 </li>
236 </template>
237 </ul>
238 <?php endif; ?>
239 <button
240 class="reaction-label has-text-color has-background"
241 data-reaction-type="<?php echo esc_attr( $_type ); ?>"
242 data-wp-on--click="actions.toggleModal"
243 type="button"
244 aria-label="<?php echo esc_attr( $aria_label ); ?>"
245 >
246 <?php echo esc_html( $reaction['label'] ); ?>
247 </button>
248 </div>
249 <?php endforeach; ?>
250 </div>
251 <?php
252 $reactions_content = ob_get_clean();
253
254 // Build modal content: reactors list (compact) + intent dialog (full-size).
255 ob_start();
256 ?>
257 <div data-wp-bind--hidden="!context.modal.isCompact">
258 <ul class="reactions-list">
259 <template data-wp-each="context.modal.items">
260 <li class="reaction-item">
261 <a data-wp-bind--href="context.item.url" target="_blank" rel="noopener noreferrer">
262 <?php if ( $show_avatars ) : ?>
263 <img
264 alt=""
265 data-wp-bind--alt="context.item.name"
266 data-wp-bind--src="context.item.avatar"
267 data-wp-on--error="callbacks.setDefaultAvatar"
268 src=""
269 />
270 <?php endif; ?>
271 <span class="reaction-name" data-wp-text="context.item.name"></span>
272 </a>
273 </li>
274 </template>
275 </ul>
276 </div>
277 <?php if ( $attributes['showActions'] ) : ?>
278 <div class="activitypub-intent-dialog" data-wp-bind--hidden="context.modal.isCompact">
279 <div class="activitypub-dialog__section">
280 <h4><?php esc_html_e( 'Post URL', 'activitypub' ); ?></h4>
281 <div class="activitypub-dialog__description">
282 <?php esc_html_e( 'Paste the post URL into the search field of your favorite open social app or platform.', 'activitypub' ); ?>
283 </div>
284 <div class="activitypub-dialog__button-group">
285 <label for="<?php echo esc_attr( $block_id . '-post-url' ); ?>" class="screen-reader-text">
286 <?php esc_html_e( 'Post URL', 'activitypub' ); ?>
287 </label>
288 <input
289 aria-readonly="true"
290 class="wp-block-search__input"
291 id="<?php echo esc_attr( $block_id . '-post-url' ); ?>"
292 readonly
293 tabindex="-1"
294 type="text"
295 data-wp-bind--value="context.postUrl"
296 />
297 <button
298 aria-label="<?php esc_attr_e( 'Copy URL to clipboard', 'activitypub' ); ?>"
299 class="wp-element-button"
300 data-wp-on--click="actions.copyPostUrl"
301 type="button"
302 >
303 <span data-wp-text="context.copyButtonText"></span>
304 </button>
305 </div>
306 </div>
307 <div class="activitypub-dialog__section">
308 <h4><?php esc_html_e( 'Your Profile', 'activitypub' ); ?></h4>
309 <div class="activitypub-dialog__description">
310 <?php esc_html_e( 'Or, if you know your own profile, we can start things that way!', 'activitypub' ); ?>
311 <?php Blocks::render_modal_help(); ?>
312 </div>
313 <div class="activitypub-dialog__button-group">
314 <label for="<?php echo esc_attr( $block_id . '-remote-profile' ); ?>" class="screen-reader-text">
315 <?php esc_html_e( 'Your Fediverse profile', 'activitypub' ); ?>
316 </label>
317 <input
318 class="wp-block-search__input"
319 data-wp-bind--aria-invalid="context.isError"
320 data-wp-bind--value="context.remoteProfile"
321 data-wp-on--input="actions.updateIntentProfile"
322 data-wp-on--keydown="actions.onIntentKeydown"
323 id="<?php echo esc_attr( $block_id . '-remote-profile' ); ?>"
324 placeholder="<?php esc_attr_e( '@username@example.com', 'activitypub' ); ?>"
325 type="text"
326 />
327 <button
328 class="wp-element-button"
329 data-wp-bind--disabled="context.isLoading"
330 data-wp-on--click="actions.submitIntent"
331 type="button"
332 >
333 <span data-wp-bind--hidden="context.isLoading"><?php esc_html_e( 'Go', 'activitypub' ); ?></span>
334 <span data-wp-bind--hidden="!context.isLoading"><?php esc_html_e( 'Loading…', 'activitypub' ); ?></span>
335 </button>
336 </div>
337 <div
338 class="activitypub-dialog__error"
339 data-wp-bind--hidden="!context.isError"
340 data-wp-text="context.errorMessage"
341 ></div>
342 <div class="activitypub-dialog__remember">
343 <label>
344 <input
345 checked
346 data-wp-bind--checked="context.shouldSaveProfile"
347 data-wp-on--change="actions.toggleRememberProfile"
348 type="checkbox"
349 />
350 <?php esc_html_e( 'Remember my profile for future interactions.', 'activitypub' ); ?>
351 </label>
352 </div>
353 </div>
354 </div>
355 <?php endif; ?>
356 <?php
357 $modal_content = ob_get_clean();
358
359 // Render the shared modal with both contents.
360 $modal_args = array(
361 'content' => $modal_content,
362 );
363
364 if ( $attributes['showActions'] ) {
365 $modal_args['title_binding'] = 'context.modal.title';
366 } else {
367 $modal_args['is_compact'] = true;
368 }
369
370 ob_start();
371 Blocks::render_modal( $modal_args );
372 $inner_content = $reactions_content . ob_get_clean();
373
374 $wrapper_attrs = array(
375 'id' => $block_id,
376 'class' => $attributes['className'] ?? '',
377 'data-wp-interactive' => 'activitypub/reactions',
378 'data-wp-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
379 'data-wp-init' => 'callbacks.initReactions',
380 );
381
382 $wrapper_attributes = get_block_wrapper_attributes( $wrapper_attrs );
383
384 // Render the block with common wrapper.
385 ?>
386 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput ?>>
387 <?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput ?>
388 <?php echo $inner_content; // phpcs:ignore WordPress.Security.EscapeOutput ?>
389 </div>
390