PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.0
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.0, at includes/comments-window/window.php

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