PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.8
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / comments-window / window.php

window.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.8, at includes/comments-window/window.php

429 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Native Comments Window: registration, template, REST fields.
4 *
5 * Mirrors the structure of the Users/Posts windows adapted for the
6 * comment collection — `/wp/v2/comments` rather than `/wp/v2/posts`.
7 * The surface is a two-pane conversation view: a Pending/All/Spam/
8 * Trash/Mine tab set over a rail of conversations, and the selected
9 * thread's full nested reply chain with a docked composer.
10 *
11 * @package WPDesktopMode
12 */
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Echoes the native Comments window's template body.
18 */
19 function desktop_mode_comments_window_render_template() {
20 ob_start();
21 ?>
22 <div class="desktop-mode-comments desktop-mode-comments--conversation" data-desktop-mode-comments-root>
23 <?php
24 /*
25 * Tab strip filters the thread rail (Pending / All / Spam /
26 * Trash / Mine). `<wpd-tabs>` owns roving `tabindex` and the
27 * `aria-selected` mirror; the bundle only listens for
28 * `wpd-tab-change`. There are no `<wpd-tabpanel>` siblings —
29 * one pane is repainted in place rather than swapped.
30 */
31 ?>
32 <wpd-tabs class="desktop-mode-comments__tabrow" value="pending"
33 label="<?php esc_attr_e( 'Comment status', 'desktop-mode' ); ?>"
34 data-desktop-mode-comments-tabs>
35 <wpd-tab value="pending"><?php esc_html_e( 'Pending', 'desktop-mode' ); ?></wpd-tab>
36 <wpd-tab value="all"><?php esc_html_e( 'All', 'desktop-mode' ); ?></wpd-tab>
37 <wpd-tab value="spam"><?php esc_html_e( 'Spam', 'desktop-mode' ); ?></wpd-tab>
38 <wpd-tab value="trash"><?php esc_html_e( 'Trash', 'desktop-mode' ); ?></wpd-tab>
39 <wpd-tab value="mine"><?php esc_html_e( 'Mine', 'desktop-mode' ); ?></wpd-tab>
40 </wpd-tabs>
41
42 <div class="desktop-mode-comments__split">
43 <?php /* Left rail: search + list of conversations (top-level comments). */ ?>
44 <aside class="desktop-mode-comments__rail" aria-label="<?php esc_attr_e( 'Conversations', 'desktop-mode' ); ?>">
45 <div class="desktop-mode-comments__search">
46 <wpd-text-field data-desktop-mode-comments-search
47 placeholder="<?php esc_attr_e( 'Search comments…', 'desktop-mode' ); ?>"></wpd-text-field>
48 </div>
49 <div class="desktop-mode-comments__list" role="list"
50 aria-label="<?php esc_attr_e( 'Conversations', 'desktop-mode' ); ?>"
51 data-desktop-mode-comments-list></div>
52 </aside>
53
54 <?php
55 /*
56 * Right pane: the selected conversation thread + composer.
57 * Deliberately NOT a live region — the whole pane is
58 * replaced on every selection and after every moderation
59 * action, so announcing it would read the entire thread
60 * aloud each time. The small status node below carries the
61 * announcements instead.
62 */
63 ?>
64 <section class="desktop-mode-comments__convo" data-desktop-mode-comments-convo></section>
65 </div>
66
67 <?php /* Single polite live region for action results ("Approved", "Reply sent"). */ ?>
68 <div class="desktop-mode-comments__status screen-reader-text" role="status" aria-live="polite"
69 data-desktop-mode-comments-status></div>
70 </div>
71 <?php
72 $html = (string) ob_get_clean();
73
74 /**
75 * Filter the native Comments window's template HTML.
76 *
77 * @param string $html Default template HTML.
78 */
79 $filtered = (string) apply_filters( 'desktop_mode_comments_window_template_html', $html );
80
81 if ( function_exists( 'desktop_mode_kses_native_window_template' ) ) {
82 echo desktop_mode_kses_native_window_template( $filtered ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes.
83 } else {
84 echo wp_kses( $filtered, wp_kses_allowed_html( 'post' ) );
85 }
86 }
87
88 /**
89 * Register the native Comments window on `init` (priority 20).
90 */
91 function desktop_mode_comments_window_register_window() {
92 if ( ! desktop_mode_comments_window_user_can_register() ) {
93 return;
94 }
95
96 $viewer_id = (int) get_current_user_id();
97
98 $window_args = array(
99 'title' => __( 'Comments', 'desktop-mode' ),
100 'icon' => 'dashicons-admin-comments',
101 'template' => 'desktop_mode_comments_window_render_template',
102 'script' => 'desktop-mode-comments-window',
103 'style' => 'desktop-mode-comments-window',
104 'width' => 1180,
105 'height' => 760,
106 'min_width' => 760,
107 'min_height' => 480,
108 'placement' => 'none',
109 'config' => array(
110 'mode' => 'comments',
111 'introSlug' => 'comments',
112 'restRoot' => esc_url_raw( rest_url() ),
113 'restNonce' => wp_create_nonce( 'wp_rest' ),
114 'commentsUrl' => esc_url_raw( rest_url( 'wp/v2/comments' ) ),
115 'currentUserId' => $viewer_id,
116 'defaultPerPage' => 20,
117 'queryArgs' => desktop_mode_comments_window_default_query_args(),
118 'introSeen' => desktop_mode_has_seen_intro( $viewer_id, 'comments' ),
119 'introUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) ),
120
121 // Capability flags surfaced to the JS — UI hides actions
122 // the viewer can't perform. Server still re-checks every
123 // mutation, so a tampered flag here changes nothing
124 // security-wise.
125 'canModerate' => current_user_can( 'moderate_comments' ),
126 'canEditComments' => current_user_can( 'edit_posts' ),
127
128 // Bulk + helper REST routes — the JS bundle reads these so
129 // a rename or namespace move stays in one place.
130 'bulkUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/bulk' ) ),
131 'replyUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/reply' ) ),
132 'insightsUrlBase' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/insights/' ) ),
133 'countsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/counts' ) ),
134 'aiSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
135 // Surface the current state so the OS Settings UI + the
136 // intro dialog can branch on it without a separate fetch.
137 // Cap-gated mirror of what the REST endpoint would return.
138 'aiModeration' => array(
139 'enabled' => desktop_mode_comments_ai_is_enabled(),
140 'providerConfigured' => desktop_mode_comments_ai_provider_configured(),
141 'canManage' => current_user_can( 'manage_options' ),
142 ),
143
144 'replyEditor' => (string) apply_filters(
145 /**
146 * Filter the reply editor implementation the bundle should mount.
147 *
148 * - `'rich'` — built-in contenteditable rich editor (default).
149 * - `'gutenberg'` — full @wordpress/block-editor (follow-up surface).
150 * - `'plain'` — plain textarea, no toolbar.
151 *
152 * @param string $editor Editor flavor slug.
153 * @param int $viewer_id Current user id.
154 */
155 'desktop_mode_comments_window_reply_editor',
156 'rich',
157 $viewer_id
158 ),
159 ),
160 );
161
162 /**
163 * Filter the args used to register the native Comments window.
164 *
165 * @param array $window_args Args passed to `desktop_mode_register_window()`.
166 */
167 $window_args = (array) apply_filters( 'desktop_mode_comments_window_args', $window_args );
168
169 $registered = desktop_mode_register_window( 'desktop-mode-comments', $window_args );
170 if ( is_wp_error( $registered ) ) {
171 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
172 error_log( '[desktop-mode] Native Comments window registration failed: ' . $registered->get_error_message() );
173 }
174 }
175 add_action( 'init', 'desktop_mode_comments_window_register_window', 20 );
176
177 /**
178 * Default REST query args for the Comments window.
179 *
180 * @return array
181 */
182 function desktop_mode_comments_window_default_query_args() {
183 // `context=edit` on `wp/v2/comments` requires `moderate_comments`
184 // — sending it as an author-without-moderate-cap 401s the entire
185 // list. Stick to `view` (every authenticated user can read it) and
186 // fall back to the REST `edit` context per-row only when the user
187 // opens the inline-edit affordance on a row they're allowed to
188 // edit.
189 $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
190 $args = array(
191 /*
192 * Exactly the fields the conversation view renders — no more.
193 * Every `desktop_mode_*` field is a computed REST field, and
194 * `desktop_mode_replies_count` runs its own `get_comments()`
195 * COUNT per row, so an over-broad `_fields` is a per-row query
196 * multiplier. The scoring fields (`spam_score`, `link_count`,
197 * `akismet`, `ai_verdict`) are NOT requested here: nothing in
198 * the conversation UI reads them, and computing them costs a
199 * meta read (or worse) per row. Re-add them via the filter
200 * below if a plugin surfaces them.
201 */
202 '_fields' =>
203 'id,post,parent,author,author_name,author_avatar_urls,'
204 . 'date_gmt,content,status,'
205 . 'desktop_mode_post_title,desktop_mode_post_link,'
206 . 'desktop_mode_can_edit,desktop_mode_can_moderate,'
207 . 'desktop_mode_replies_count',
208 'context' => $context,
209 'per_page' => 20,
210 // 'hold' = pending. Use the wp/v2 status names where they differ
211 // from core's: 'hold' / 'approve' / 'spam' / 'trash'.
212 'status' => 'hold',
213 );
214
215 /**
216 * Filter the default outbound REST query args for the Comments window.
217 *
218 * @param array $args Default args.
219 */
220 return (array) apply_filters( 'desktop_mode_comments_window_query_args', $args );
221 }
222
223 /**
224 * Register the Comments-window REST fields on the `comment` resource.
225 *
226 * Fields are computed lazily — none of them runs unless the bundle
227 * explicitly requests them via `_fields`.
228 */
229 function desktop_mode_comments_window_register_rest_fields() {
230 register_rest_field(
231 'comment',
232 'desktop_mode_post_title',
233 array(
234 'get_callback' => static function ( $row ) {
235 $post_id = isset( $row['post'] ) ? (int) $row['post'] : 0;
236 if ( $post_id <= 0 ) {
237 return '';
238 }
239 $post = get_post( $post_id );
240 return $post ? (string) get_the_title( $post ) : '';
241 },
242 'schema' => array(
243 'description' => __( 'Title of the post the comment is attached to.', 'desktop-mode' ),
244 'type' => 'string',
245 'context' => array( 'view', 'edit' ),
246 'readonly' => true,
247 ),
248 )
249 );
250
251 register_rest_field(
252 'comment',
253 'desktop_mode_post_link',
254 array(
255 'get_callback' => static function ( $row ) {
256 $post_id = isset( $row['post'] ) ? (int) $row['post'] : 0;
257 if ( $post_id <= 0 ) {
258 return '';
259 }
260 return (string) get_permalink( $post_id );
261 },
262 'schema' => array(
263 'description' => __( 'Permalink of the post the comment is attached to.', 'desktop-mode' ),
264 'type' => 'string',
265 'format' => 'uri',
266 'context' => array( 'view', 'edit' ),
267 'readonly' => true,
268 ),
269 )
270 );
271
272 register_rest_field(
273 'comment',
274 'desktop_mode_spam_score',
275 array(
276 'get_callback' => static function ( $row ) {
277 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
278 if ( $id <= 0 ) {
279 return 0;
280 }
281 return (int) desktop_mode_comments_window_spam_score( $id );
282 },
283 'schema' => array(
284 'description' => __( 'Spam confidence score, 0–100.', 'desktop-mode' ),
285 'type' => 'integer',
286 'minimum' => 0,
287 'maximum' => 100,
288 'context' => array( 'view', 'edit' ),
289 'readonly' => true,
290 ),
291 )
292 );
293
294 register_rest_field(
295 'comment',
296 'desktop_mode_link_count',
297 array(
298 'get_callback' => static function ( $row ) {
299 $content = isset( $row['content']['raw'] )
300 ? (string) $row['content']['raw']
301 : ( isset( $row['content']['rendered'] ) ? (string) $row['content']['rendered'] : '' );
302 return (int) preg_match_all( '#https?://#i', $content );
303 },
304 'schema' => array(
305 'description' => __( 'Number of links in the comment.', 'desktop-mode' ),
306 'type' => 'integer',
307 'minimum' => 0,
308 'context' => array( 'view', 'edit' ),
309 'readonly' => true,
310 ),
311 )
312 );
313
314 register_rest_field(
315 'comment',
316 'desktop_mode_can_edit',
317 array(
318 'get_callback' => static function ( $row ) {
319 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
320 return $id > 0 && current_user_can( 'edit_comment', $id );
321 },
322 'schema' => array(
323 'description' => __( 'Whether the requester can edit this comment.', 'desktop-mode' ),
324 'type' => 'boolean',
325 'context' => array( 'view', 'edit' ),
326 'readonly' => true,
327 ),
328 )
329 );
330
331 register_rest_field(
332 'comment',
333 'desktop_mode_can_moderate',
334 array(
335 'get_callback' => static function () {
336 return current_user_can( 'moderate_comments' );
337 },
338 'schema' => array(
339 'description' => __( 'Whether the requester can moderate comments globally.', 'desktop-mode' ),
340 'type' => 'boolean',
341 'context' => array( 'view', 'edit' ),
342 'readonly' => true,
343 ),
344 )
345 );
346
347 register_rest_field(
348 'comment',
349 'desktop_mode_replies_count',
350 array(
351 'get_callback' => static function ( $row ) {
352 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
353 if ( $id <= 0 ) {
354 return 0;
355 }
356 return (int) get_comments(
357 array(
358 'parent' => $id,
359 'count' => true,
360 'status' => 'all',
361 )
362 );
363 },
364 'schema' => array(
365 'description' => __( 'Direct-reply count for this comment.', 'desktop-mode' ),
366 'type' => 'integer',
367 'minimum' => 0,
368 'context' => array( 'view', 'edit' ),
369 'readonly' => true,
370 ),
371 )
372 );
373
374 register_rest_field(
375 'comment',
376 'desktop_mode_ai_verdict',
377 array(
378 'get_callback' => static function ( $row ) {
379 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
380 if ( $id <= 0 || ! function_exists( 'desktop_mode_ai_get_meta' ) ) {
381 return null;
382 }
383 $meta = desktop_mode_ai_get_meta( 'comment', $id );
384 if ( ! is_array( $meta ) ) {
385 return null;
386 }
387 return array(
388 'spam' => ! empty( $meta['spam'] ),
389 'harmful' => ! empty( $meta['harmful'] ),
390 'topic' => isset( $meta['topic'] ) ? (string) $meta['topic'] : '',
391 'summary' => isset( $meta['ai_summary'] ) ? (string) $meta['ai_summary'] : '',
392 'analyzedAt' => isset( $meta['analyzed_at'] ) ? (int) $meta['analyzed_at'] : 0,
393 );
394 },
395 'schema' => array(
396 'description' => __( 'AI moderation verdict for this comment, when analyzed (null otherwise).', 'desktop-mode' ),
397 'type' => array( 'object', 'null' ),
398 'context' => array( 'view', 'edit' ),
399 'readonly' => true,
400 ),
401 )
402 );
403
404 register_rest_field(
405 'comment',
406 'desktop_mode_akismet',
407 array(
408 'get_callback' => static function ( $row ) {
409 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
410 if ( $id <= 0 ) {
411 return null;
412 }
413 $result = (string) get_comment_meta( $id, 'akismet_result', true );
414 if ( '' === $result ) {
415 return null;
416 }
417 return $result; // 'true' | 'false' | 'pending'.
418 },
419 'schema' => array(
420 'description' => __( 'Akismet verdict if the plugin is installed (null otherwise).', 'desktop-mode' ),
421 'type' => array( 'string', 'null' ),
422 'context' => array( 'view', 'edit' ),
423 'readonly' => true,
424 ),
425 )
426 );
427 }
428 add_action( 'rest_api_init', 'desktop_mode_comments_window_register_rest_fields' );
429