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

430 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — 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 OpenStation
12 */
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Echoes the native Comments window's template body.
18 */
19 function openstation_comments_window_render_template() {
20 ob_start();
21 ?>
22 <div class="os-comments os-comments--conversation" data-os-comments-root>
23 <?php
24 /*
25 * Tab strip filters the thread rail (Pending / All / Spam /
26 * Trash / Mine). `<os-tabs>` owns roving `tabindex` and the
27 * `aria-selected` mirror; the bundle only listens for
28 * `os-tab-change`. There are no `<os-tabpanel>` siblings —
29 * one pane is repainted in place rather than swapped.
30 */
31 ?>
32 <os-tabs class="os-comments__tabrow" value="pending"
33 label="<?php esc_attr_e( 'Comment status', 'desktop-mode' ); ?>"
34 data-os-comments-tabs>
35 <os-tab value="pending"><?php esc_html_e( 'Pending', 'desktop-mode' ); ?></os-tab>
36 <os-tab value="all"><?php esc_html_e( 'All', 'desktop-mode' ); ?></os-tab>
37 <os-tab value="spam"><?php esc_html_e( 'Spam', 'desktop-mode' ); ?></os-tab>
38 <os-tab value="trash"><?php esc_html_e( 'Trash', 'desktop-mode' ); ?></os-tab>
39 <os-tab value="mine"><?php esc_html_e( 'Mine', 'desktop-mode' ); ?></os-tab>
40 </os-tabs>
41
42 <div class="os-comments__split">
43 <?php /* Left rail: search + list of conversations (top-level comments). */ ?>
44 <aside class="os-comments__rail" aria-label="<?php esc_attr_e( 'Conversations', 'desktop-mode' ); ?>">
45 <div class="os-comments__search">
46 <os-text-field data-os-comments-search
47 placeholder="<?php esc_attr_e( 'Search comments…', 'desktop-mode' ); ?>"></os-text-field>
48 </div>
49 <div class="os-comments__list" role="list"
50 aria-label="<?php esc_attr_e( 'Conversations', 'desktop-mode' ); ?>"
51 data-os-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="os-comments__convo" data-os-comments-convo></section>
65 </div>
66
67 <?php /* Single polite live region for action results ("Approved", "Reply sent"). */ ?>
68 <div class="os-comments__status screen-reader-text" role="status" aria-live="polite"
69 data-os-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( 'openstation_comments_window_template_html', $html );
80
81 if ( function_exists( 'openstation_kses_native_window_template' ) ) {
82 echo openstation_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 openstation_comments_window_register_window() {
92 if ( ! openstation_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' => 'openstation_comments_window_render_template',
102 'script' => 'os-comments-window',
103 'style' => 'os-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' => openstation_comments_window_default_query_args(),
118 'introSeen' => openstation_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' => openstation_comments_ai_is_enabled(),
140 'providerConfigured' => openstation_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 'openstation_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 `openstation_register_window()`.
166 */
167 $window_args = (array) apply_filters( 'openstation_comments_window_args', $window_args );
168
169 $registered = openstation_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( '[openstation] Native Comments window registration failed: ' . $registered->get_error_message() );
173 }
174 }
175 add_action( 'init', 'openstation_comments_window_register_window', 20 );
176
177 /**
178 * Default REST query args for the Comments window.
179 *
180 * @return array
181 */
182 function openstation_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 /*
193 * Exactly the fields the conversation view renders — no more.
194 * Every `openstation_*` field is a computed REST field, and
195 * `openstation_replies_count` runs its own `get_comments()`
196 * COUNT per row, so an over-broad `_fields` is a per-row query
197 * multiplier. The scoring fields (`spam_score`, `link_count`,
198 * `akismet`, `ai_verdict`) are NOT requested here: nothing in
199 * the conversation UI reads them, and computing them costs a
200 * meta read (or worse) per row. Re-add them via the filter
201 * below if a plugin surfaces them.
202 */
203 '_fields' =>
204 'id,post,parent,author,author_name,author_avatar_urls,'
205 . 'date_gmt,content,status,'
206 . 'openstation_post_title,openstation_post_link,'
207 . 'openstation_can_edit,openstation_can_moderate,'
208 . 'openstation_replies_count',
209 'context' => $context,
210 'per_page' => 20,
211 // 'hold' = pending. Use the wp/v2 status names where they differ
212 // from core's: 'hold' / 'approve' / 'spam' / 'trash'.
213 'status' => 'hold',
214 );
215
216 /**
217 * Filter the default outbound REST query args for the Comments window.
218 *
219 * @param array $args Default args.
220 */
221 return (array) apply_filters( 'openstation_comments_window_query_args', $args );
222 }
223
224 /**
225 * Register the Comments-window REST fields on the `comment` resource.
226 *
227 * Fields are computed lazily — none of them runs unless the bundle
228 * explicitly requests them via `_fields`.
229 */
230 function openstation_comments_window_register_rest_fields() {
231 register_rest_field(
232 'comment',
233 'openstation_post_title',
234 array(
235 'get_callback' => static function ( $row ) {
236 $post_id = isset( $row['post'] ) ? (int) $row['post'] : 0;
237 if ( $post_id <= 0 ) {
238 return '';
239 }
240 $post = get_post( $post_id );
241 return $post ? (string) get_the_title( $post ) : '';
242 },
243 'schema' => array(
244 'description' => __( 'Title of the post the comment is attached to.', 'desktop-mode' ),
245 'type' => 'string',
246 'context' => array( 'view', 'edit' ),
247 'readonly' => true,
248 ),
249 )
250 );
251
252 register_rest_field(
253 'comment',
254 'openstation_post_link',
255 array(
256 'get_callback' => static function ( $row ) {
257 $post_id = isset( $row['post'] ) ? (int) $row['post'] : 0;
258 if ( $post_id <= 0 ) {
259 return '';
260 }
261 return (string) get_permalink( $post_id );
262 },
263 'schema' => array(
264 'description' => __( 'Permalink of the post the comment is attached to.', 'desktop-mode' ),
265 'type' => 'string',
266 'format' => 'uri',
267 'context' => array( 'view', 'edit' ),
268 'readonly' => true,
269 ),
270 )
271 );
272
273 register_rest_field(
274 'comment',
275 'openstation_spam_score',
276 array(
277 'get_callback' => static function ( $row ) {
278 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
279 if ( $id <= 0 ) {
280 return 0;
281 }
282 return (int) openstation_comments_window_spam_score( $id );
283 },
284 'schema' => array(
285 'description' => __( 'Spam confidence score, 0–100.', 'desktop-mode' ),
286 'type' => 'integer',
287 'minimum' => 0,
288 'maximum' => 100,
289 'context' => array( 'view', 'edit' ),
290 'readonly' => true,
291 ),
292 )
293 );
294
295 register_rest_field(
296 'comment',
297 'openstation_link_count',
298 array(
299 'get_callback' => static function ( $row ) {
300 $content = isset( $row['content']['raw'] )
301 ? (string) $row['content']['raw']
302 : ( isset( $row['content']['rendered'] ) ? (string) $row['content']['rendered'] : '' );
303 return (int) preg_match_all( '#https?://#i', $content );
304 },
305 'schema' => array(
306 'description' => __( 'Number of links in the comment.', 'desktop-mode' ),
307 'type' => 'integer',
308 'minimum' => 0,
309 'context' => array( 'view', 'edit' ),
310 'readonly' => true,
311 ),
312 )
313 );
314
315 register_rest_field(
316 'comment',
317 'openstation_can_edit',
318 array(
319 'get_callback' => static function ( $row ) {
320 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
321 return $id > 0 && current_user_can( 'edit_comment', $id );
322 },
323 'schema' => array(
324 'description' => __( 'Whether the requester can edit this comment.', 'desktop-mode' ),
325 'type' => 'boolean',
326 'context' => array( 'view', 'edit' ),
327 'readonly' => true,
328 ),
329 )
330 );
331
332 register_rest_field(
333 'comment',
334 'openstation_can_moderate',
335 array(
336 'get_callback' => static function () {
337 return current_user_can( 'moderate_comments' );
338 },
339 'schema' => array(
340 'description' => __( 'Whether the requester can moderate comments globally.', 'desktop-mode' ),
341 'type' => 'boolean',
342 'context' => array( 'view', 'edit' ),
343 'readonly' => true,
344 ),
345 )
346 );
347
348 register_rest_field(
349 'comment',
350 'openstation_replies_count',
351 array(
352 'get_callback' => static function ( $row ) {
353 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
354 if ( $id <= 0 ) {
355 return 0;
356 }
357 return (int) get_comments(
358 array(
359 'parent' => $id,
360 'count' => true,
361 'status' => 'all',
362 )
363 );
364 },
365 'schema' => array(
366 'description' => __( 'Direct-reply count for this comment.', 'desktop-mode' ),
367 'type' => 'integer',
368 'minimum' => 0,
369 'context' => array( 'view', 'edit' ),
370 'readonly' => true,
371 ),
372 )
373 );
374
375 register_rest_field(
376 'comment',
377 'openstation_ai_verdict',
378 array(
379 'get_callback' => static function ( $row ) {
380 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
381 if ( $id <= 0 || ! function_exists( 'openstation_ai_get_meta' ) ) {
382 return null;
383 }
384 $meta = openstation_ai_get_meta( 'comment', $id );
385 if ( ! is_array( $meta ) ) {
386 return null;
387 }
388 return array(
389 'spam' => ! empty( $meta['spam'] ),
390 'harmful' => ! empty( $meta['harmful'] ),
391 'topic' => isset( $meta['topic'] ) ? (string) $meta['topic'] : '',
392 'summary' => isset( $meta['ai_summary'] ) ? (string) $meta['ai_summary'] : '',
393 'analyzedAt' => isset( $meta['analyzed_at'] ) ? (int) $meta['analyzed_at'] : 0,
394 );
395 },
396 'schema' => array(
397 'description' => __( 'AI moderation verdict for this comment, when analyzed (null otherwise).', 'desktop-mode' ),
398 'type' => array( 'object', 'null' ),
399 'context' => array( 'view', 'edit' ),
400 'readonly' => true,
401 ),
402 )
403 );
404
405 register_rest_field(
406 'comment',
407 'openstation_akismet',
408 array(
409 'get_callback' => static function ( $row ) {
410 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
411 if ( $id <= 0 ) {
412 return null;
413 }
414 $result = (string) get_comment_meta( $id, 'akismet_result', true );
415 if ( '' === $result ) {
416 return null;
417 }
418 return $result; // 'true' | 'false' | 'pending'.
419 },
420 'schema' => array(
421 'description' => __( 'Akismet verdict if the plugin is installed (null otherwise).', 'desktop-mode' ),
422 'type' => array( 'string', 'null' ),
423 'context' => array( 'view', 'edit' ),
424 'readonly' => true,
425 ),
426 )
427 );
428 }
429 add_action( 'rest_api_init', 'openstation_comments_window_register_rest_fields' );
430