PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / inc / Services / FeatureAnnounce.php
presto-player / inc / Services Last commit date
API 1 week ago Abilities 1 week ago Blocks 3 weeks ago License 1 month ago OAuth 1 week ago AdminNotice.php 2 months ago AdminNotices.php 2 months ago AjaxActions.php 2 months ago Blocks.php 2 years ago Compatibility.php 11 months ago FeatureAnnounce.php 1 week ago Learn.php 1 week ago Menu.php 1 week ago Migrations.php 2 years ago NpsSurvey.php 6 months ago Onboarding.php 2 months ago Player.php 3 weeks ago PluginInstaller.php 1 week ago PreloadService.php 1 year ago ProCompatibility.php 3 weeks ago ReusableVideos.php 1 month ago RewriteRulesManager.php 2 years ago Scripts.php 1 month ago Settings.php 3 months ago Shortcodes.php 1 week ago Streamer.php 2 months ago Translation.php 3 weeks ago Usage.php 1 week ago VideoPostType.php 3 weeks ago
FeatureAnnounce.php
823 lines
1 <?php
2 /**
3 * Feature announcement modal shown once after a plugin update.
4 *
5 * @package PrestoPlayer\Services
6 */
7
8 namespace PrestoPlayer\Services;
9
10 /**
11 * Shows a one-time modal introducing a major new feature after the user
12 * updates the plugin. Currently announces AI Abilities.
13 */
14 class FeatureAnnounce {
15
16 /**
17 * Bump this when announcing a different feature — a new id means everyone
18 * sees the modal again, even people who dismissed the previous one.
19 */
20 const ANNOUNCEMENT_ID = 'wp_abilities_v1';
21
22 /**
23 * The release that introduced the feature being announced. Sites already on
24 * this version or newer never installed without it, so they don't need telling.
25 */
26 const ANNOUNCED_IN_VERSION = '4.4.0';
27
28 /**
29 * Last plugin version this install has seen. Empty on a fresh install.
30 */
31 const SEEN_VERSION_OPTION = 'presto_player_seen_version';
32
33 /**
34 * Announcement id waiting to be shown, set when the version changes.
35 */
36 const PENDING_OPTION = 'presto_player_pending_announcement';
37
38 /**
39 * Announcement ids the current user has closed.
40 */
41 const DISMISSED_META = 'presto_player_dismissed_announcements';
42
43 /**
44 * Nonce action for the dismiss request.
45 */
46 const NONCE_ACTION = 'presto_dismiss_announcement';
47
48 /**
49 * Resolved plugin version, cached for the request.
50 *
51 * @var string|null
52 */
53 protected $version = null;
54
55 /**
56 * Register hooks.
57 *
58 * @return void
59 */
60 public function register() {
61 add_action( 'admin_init', array( $this, 'trackVersion' ) );
62 add_action( 'admin_footer', array( $this, 'render' ) );
63 add_action( 'wp_ajax_presto_dismiss_announcement', array( $this, 'dismiss' ) );
64 }
65
66 /**
67 * Queue the announcement when the running version changes.
68 *
69 * @return void
70 */
71 public function trackVersion() {
72 $seen = get_option( self::SEEN_VERSION_OPTION, '' );
73 if ( $this->version() === $seen ) {
74 return;
75 }
76
77 // No recorded version means either a brand new install or the first load
78 // after updating into the release that added this class.
79 // onboarding_completed tells the two apart — it is backfilled for
80 // pre-4.2.0 installs, so an established site always has it set.
81 // Bail before stamping: a site whose backfill missed it would otherwise be
82 // marked as caught up and could never be told about the feature.
83 if ( '' === $seen && 'yes' !== get_option( 'presto_player_onboarding_completed', 'no' ) ) {
84 return;
85 }
86
87 update_option( self::SEEN_VERSION_OPTION, $this->version() );
88
89 // Only announce to sites that were here before the feature shipped. Without
90 // this floor, a site that installs 4.5 in 2027 and updates to 4.5.1 gets a
91 // full-screen "New in Presto Player" for something that predates it.
92 if ( '' !== $seen && version_compare( $seen, self::ANNOUNCED_IN_VERSION, '>=' ) ) {
93 return;
94 }
95
96 update_option( self::PENDING_OPTION, self::ANNOUNCEMENT_ID );
97 }
98
99 /**
100 * Whether the modal should be printed for this request.
101 *
102 * @return bool
103 */
104 protected function shouldShow() {
105 // manage_options, not the menu's publish_posts: the settings REST route
106 // rejects anyone lower, so an author following the CTA just lands on a
107 // "Couldn't load settings" page.
108 if ( ! current_user_can( 'manage_options' ) ) {
109 return false;
110 }
111
112 // Nothing to announce on WordPress versions without the Abilities API.
113 if ( ! function_exists( 'wp_register_ability' ) ) {
114 return false;
115 }
116
117 // Only after an update queued it — see trackVersion().
118 if ( self::ANNOUNCEMENT_ID !== get_option( self::PENDING_OPTION, '' ) ) {
119 return false;
120 }
121
122 if ( ! $this->onPrestoScreen() ) {
123 return false;
124 }
125
126 return ! in_array( self::ANNOUNCEMENT_ID, $this->dismissed(), true );
127 }
128
129 /**
130 * Whether we are on one of Presto Player's own admin screens.
131 *
132 * The announcement belongs inside the plugin — it should not follow someone
133 * around the rest of WordPress.
134 *
135 * @return bool
136 */
137 protected function onPrestoScreen() {
138 if ( ! function_exists( 'get_current_screen' ) ) {
139 return false;
140 }
141
142 $screen = get_current_screen();
143 if ( ! $screen ) {
144 return false;
145 }
146
147 // Media Hub list and editor.
148 if ( 'pp_video_block' === $screen->post_type ) {
149 return true;
150 }
151
152 // Every menu page hangs off the presto-dashboard slug, so the screen id
153 // carries it — toplevel_page_presto-dashboard and friends.
154 return false !== strpos( $screen->id, 'presto' );
155 }
156
157 /**
158 * Print the modal.
159 *
160 * @return void
161 */
162 public function render() {
163 if ( ! $this->shouldShow() ) {
164 return;
165 }
166
167 $settings_url = admin_url( 'admin.php?page=presto-dashboard&tab=Settings&section=mcp' );
168
169 $feats = array(
170 array(
171 'icon' => 'wand',
172 'label' => __( 'Create a video from a link', 'presto-player' ),
173 ),
174 array(
175 'icon' => 'chart',
176 'label' => __( 'Ask for your analytics', 'presto-player' ),
177 ),
178 array(
179 'icon' => 'shield',
180 'label' => __( 'Read-only until you allow more', 'presto-player' ),
181 ),
182 );
183
184 ?>
185 <div class="presto-announce" role="dialog" aria-modal="true" aria-labelledby="presto-announce-title">
186 <div class="presto-announce__box" tabindex="-1">
187 <button type="button" class="presto-announce__x" aria-label="<?php esc_attr_e( 'Close', 'presto-player' ); ?>">
188 <svg viewBox="0 0 20 20" width="15" height="15" aria-hidden="true" focusable="false"><path d="M5 5l10 10M15 5L5 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
189 </button>
190
191 <?php $this->art(); ?>
192
193 <div class="presto-announce__body">
194 <p class="presto-announce__tag">
195 <span class="presto-announce__tag-new">
196 <?php esc_html_e( 'New in Presto Player', 'presto-player' ); ?>
197 </span>
198 <span class="presto-announce__tag-ver"><?php echo esc_html( $this->shortVersion() ); ?></span>
199 </p>
200
201 <h2 class="presto-announce__title" id="presto-announce-title">
202 <?php esc_html_e( 'Talk to your video library', 'presto-player' ); ?>
203 </h2>
204
205 <p class="presto-announce__text">
206 <?php esc_html_e( 'Connect Claude, ChatGPT or any MCP client to this site, then ask for what you want in plain words. You decide how much it is allowed to change.', 'presto-player' ); ?>
207 </p>
208
209 <ul class="presto-announce__feats">
210 <?php foreach ( $feats as $feat ) : ?>
211 <li>
212 <?php $this->icon( $feat['icon'] ); ?>
213 <span><?php echo esc_html( $feat['label'] ); ?></span>
214 </li>
215 <?php endforeach; ?>
216 </ul>
217
218 <div class="presto-announce__actions">
219 <a class="presto-announce__cta" href="<?php echo esc_url( $settings_url ); ?>">
220 <?php esc_html_e( 'Set up AI Abilities', 'presto-player' ); ?>
221 <svg viewBox="0 0 20 20" width="15" height="15" aria-hidden="true" focusable="false"><path d="M4 10h11m-4.5-4.5L15 10l-4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
222 </a>
223 <button type="button" class="presto-announce__close">
224 <?php esc_html_e( 'Not now', 'presto-player' ); ?>
225 </button>
226 </div>
227 </div>
228 </div>
229 </div>
230 <?php
231
232 $this->styles();
233 $this->script();
234 }
235
236 /**
237 * Small line icon for a feature row.
238 *
239 * @param string $name Icon key.
240 * @return void
241 */
242 protected function icon( $name ) {
243 $paths = array(
244 'wand' => '<path d="M4 16l9-9m-2-3l.9 2.1L14 7l-2.1.9L11 10l-.9-2.1L8 7l2.1-.9zM16 12l.6 1.4L18 14l-1.4.6L16 16l-.6-1.4L14 14l1.4-.6z"/>',
245 'chart' => '<path d="M4 16V9m4 7V5m4 11v-5m4 5V8"/>',
246 'shield' => '<path d="M10 3.5l5 1.8v4.2c0 3.1-2 5.6-5 7-3-1.4-5-3.9-5-7V5.3z"/><path d="M7.8 10.2L9.4 12l3-3.4"/>',
247 );
248
249 if ( empty( $paths[ $name ] ) ) {
250 return;
251 }
252
253 printf(
254 '<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">%s</svg>',
255 $paths[ $name ] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup from the map above.
256 );
257 }
258
259 /**
260 * Store the dismissal against the current user.
261 *
262 * @return void
263 */
264 public function dismiss() {
265 $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
266 if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
267 wp_send_json_error();
268 }
269
270 if ( ! current_user_can( 'manage_options' ) ) {
271 wp_send_json_error();
272 }
273
274 $dismissed = $this->dismissed();
275 if ( ! in_array( self::ANNOUNCEMENT_ID, $dismissed, true ) ) {
276 $dismissed[] = self::ANNOUNCEMENT_ID;
277 update_user_meta( get_current_user_id(), self::DISMISSED_META, $dismissed );
278 }
279
280 wp_send_json_success();
281 }
282
283 /**
284 * Announcement ids this user has already closed.
285 *
286 * @return string[]
287 */
288 protected function dismissed() {
289 $dismissed = get_user_meta( get_current_user_id(), self::DISMISSED_META, true );
290 return is_array( $dismissed ) ? $dismissed : array();
291 }
292
293 /**
294 * Full plugin version. There is no version constant in the free plugin, so
295 * fall back to the plugin header the same way the OAuth endpoints do.
296 *
297 * @return string
298 */
299 protected function version() {
300 if ( null !== $this->version ) {
301 return $this->version;
302 }
303
304 if ( defined( 'PRESTO_PLAYER_VERSION' ) ) {
305 $this->version = (string) constant( 'PRESTO_PLAYER_VERSION' );
306 return $this->version;
307 }
308
309 if ( ! function_exists( 'get_plugin_data' ) ) {
310 require_once ABSPATH . 'wp-admin/includes/plugin.php';
311 }
312
313 $data = get_plugin_data( PRESTO_PLAYER_PLUGIN_FILE, false, false );
314 $this->version = empty( $data['Version'] ) ? '' : (string) $data['Version'];
315
316 return $this->version;
317 }
318
319 /**
320 * Version without the patch part, for display — 4.3.2 becomes 4.3.
321 *
322 * @return string
323 */
324 protected function shortVersion() {
325 $parts = explode( '.', $this->version() );
326 return implode( '.', array_slice( $parts, 0, 2 ) );
327 }
328
329 /**
330 * Illustration: the Media Hub list with an assistant adding a video to it.
331 *
332 * Sample content is intentionally not translatable. It is a product mock
333 * behind aria-hidden — the same as a screenshot — so translating it would
334 * only add contextless strings to the .pot file.
335 *
336 * @return void
337 */
338 protected function art() {
339 ?>
340 <div class="presto-announce__art" aria-hidden="true">
341 <ul class="presto-announce__list">
342 <li>
343 <span>Onboarding &middot; Lesson 1</span>
344 <em>YouTube &middot; 4:21</em>
345 </li>
346 <li>
347 <span>Onboarding &middot; Lesson 2</span>
348 <em>Bunny &middot; 12:04</em>
349 </li>
350 <li class="presto-announce__list-new">
351 <span>Product Demo 2026</span>
352 <em>YouTube &middot; 3:38</em>
353 </li>
354 </ul>
355
356 <div class="presto-announce__chat">
357 <p class="presto-announce__chat-who">Claude</p>
358 <p class="presto-announce__chat-ask">Add this video as &ldquo;Product Demo 2026&rdquo; and give me the shortcode.</p>
359 <p class="presto-announce__chat-done">Done &mdash; [presto_player id=4182]</p>
360 <p class="presto-announce__chips">
361 <span>create-video-youtube</span>
362 <span>get-video-shortcode</span>
363 </p>
364 </div>
365 </div>
366 <?php
367 }
368
369 /**
370 * Modal styles. Printed inline because this only renders once per user.
371 *
372 * @return void
373 */
374 protected function styles() {
375 ?>
376 <style>
377 .presto-announce {
378 --presto-ink: #101223;
379 --presto-muted: #656D88;
380 --presto-line: rgba( 16, 18, 35, .07 );
381 --presto-blue: #3058E5;
382 --presto-violet: #9A20F8;
383 --presto-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
384
385 position: fixed;
386 inset: 0;
387 z-index: 160000;
388 display: flex;
389 align-items: center;
390 justify-content: center;
391 padding: 24px;
392 /* Scroll the overlay, never the card — a scrollbar inside the panel
393 looks like a mistake. */
394 overflow-y: auto;
395 background: rgba( 16, 18, 35, .46 );
396 animation: presto-announce-veil .22s ease both;
397 }
398
399 .presto-announce__box {
400 position: relative;
401 overflow: hidden;
402 width: 100%;
403 max-width: 520px;
404 /* Centres while allowing the overlay to scroll instead of clipping. */
405 margin: auto;
406 border-radius: 16px;
407 background: #fff;
408 /* Layered rather than one big blur — reads lifted, not stamped on. */
409 box-shadow:
410 0 1px 2px rgba( 16, 18, 35, .05 ),
411 0 12px 24px -10px rgba( 16, 18, 35, .14 ),
412 0 44px 72px -32px rgba( 16, 18, 35, .34 );
413 animation: presto-announce-rise .34s cubic-bezier( .2, .8, .2, 1 ) both;
414 }
415
416 /* Holds focus for aria-modal without drawing a ring — it's a container,
417 not something you interact with. */
418 .presto-announce__box:focus {
419 outline: none;
420 }
421
422 /* The only brand wash: a 2px edge, flush to the top. */
423 .presto-announce__box::before {
424 content: "";
425 position: absolute;
426 top: 0;
427 left: 0;
428 right: 0;
429 height: 2px;
430 background: linear-gradient( 90deg, var( --presto-violet ), #5B37F0 52%, var( --presto-blue ) );
431 }
432
433 /* A product shot in markup, matching the landing page: the library list
434 with an assistant working against it. Deliberately dark so the graphics
435 read as a separate section from the content below. */
436 .presto-announce__art {
437 position: relative;
438 padding: 28px 26px 26px;
439 background:
440 radial-gradient( 82% 122% at 4% -14%, rgba( 154, 32, 248, .42 ), transparent 62% ),
441 radial-gradient( 70% 108% at 99% -6%, rgba( 48, 88, 229, .4 ), transparent 60% ),
442 #0D0F1E;
443 }
444
445 .presto-announce__list {
446 margin: 0;
447 padding: 6px;
448 border-radius: 10px;
449 background: #fff;
450 box-shadow: 0 12px 30px -14px rgba( 5, 6, 15, .75 );
451 list-style: none;
452 }
453
454 .presto-announce__list li {
455 display: flex;
456 align-items: center;
457 justify-content: space-between;
458 gap: 12px;
459 margin: 0;
460 padding: 9px 10px;
461 border-radius: 7px;
462 font-size: 12.5px;
463 color: var( --presto-ink );
464 }
465
466 .presto-announce__list em {
467 flex: none;
468 font-family: var( --presto-mono );
469 font-size: 10.5px;
470 font-style: normal;
471 color: var( --presto-muted );
472 }
473
474 /* The row the assistant just created. */
475 .presto-announce__list-new {
476 background: rgba( 48, 88, 229, .07 );
477 font-weight: 600;
478 animation: presto-announce-row .45s ease 1.5s both;
479 }
480
481 .presto-announce__chat {
482 position: relative;
483 width: 78%;
484 margin: -16px 0 0 auto;
485 padding: 13px 14px;
486 border-radius: 11px;
487 background: #fff;
488 box-shadow: 0 16px 36px -14px rgba( 5, 6, 15, .8 );
489 animation: presto-announce-chat .45s cubic-bezier( .2, .8, .2, 1 ) .5s both;
490 }
491
492 .presto-announce__chat-who {
493 display: flex;
494 align-items: center;
495 gap: 7px;
496 margin: 0 0 9px;
497 font-size: 11.5px;
498 font-weight: 600;
499 color: var( --presto-ink );
500 }
501
502 .presto-announce__chat-who::before {
503 content: "";
504 width: 16px;
505 height: 16px;
506 border-radius: 5px;
507 background: linear-gradient( 135deg, var( --presto-violet ), var( --presto-blue ) );
508 }
509
510 .presto-announce__chat-ask {
511 margin: 0 0 8px;
512 padding: 8px 10px;
513 border-radius: 7px;
514 background: rgba( 16, 18, 35, .04 );
515 font-size: 11.5px;
516 line-height: 1.5;
517 color: var( --presto-ink );
518 }
519
520 .presto-announce__chat-done {
521 margin: 0 0 9px;
522 font-family: var( --presto-mono );
523 font-size: 10.5px;
524 line-height: 1.5;
525 color: var( --presto-muted );
526 animation: presto-announce-done .4s ease 1.5s both;
527 }
528
529 /* The abilities that ran — named, because that is what the feature is. */
530 .presto-announce__chips {
531 display: flex;
532 flex-wrap: wrap;
533 gap: 6px;
534 margin: 0;
535 animation: presto-announce-done .4s ease 1.7s both;
536 }
537
538 .presto-announce__chips span {
539 padding: 3px 7px;
540 border-radius: 5px;
541 background: #ECFDF3;
542 font-family: var( --presto-mono );
543 font-size: 10px;
544 color: #067647;
545 }
546
547 .presto-announce__body {
548 padding: 24px 26px 22px;
549 }
550
551 /* Machine voice: version and feature name share the mono face. */
552 .presto-announce__tag {
553 display: flex;
554 align-items: center;
555 gap: 8px;
556 margin: 0 0 12px;
557 font-family: var( --presto-mono );
558 font-size: 10.5px;
559 color: var( --presto-muted );
560 }
561
562 .presto-announce__tag-new {
563 padding: 3px 7px;
564 border-radius: 4px;
565 background: rgba( 48, 88, 229, .09 );
566 color: var( --presto-blue );
567 font-weight: 600;
568 letter-spacing: .06em;
569 text-transform: uppercase;
570 }
571
572 .presto-announce__tag-name::before {
573 content: "\00b7";
574 margin-right: 8px;
575 opacity: .45;
576 }
577
578 .presto-announce__title {
579 margin: 0 0 9px;
580 font-size: 25px;
581 font-weight: 640;
582 line-height: 1.18;
583 letter-spacing: -.026em;
584 color: var( --presto-ink );
585 }
586
587 .presto-announce__text {
588 margin: 0 0 18px;
589 font-size: 13.5px;
590 line-height: 1.64;
591 color: var( --presto-muted );
592 }
593
594 /* Two features side by side, the way the settings screen lists them. */
595 .presto-announce__feats {
596 display: flex;
597 flex-wrap: wrap;
598 gap: 10px 26px;
599 margin: 0 0 20px;
600 padding: 0;
601 list-style: none;
602 }
603
604 .presto-announce__feats li {
605 display: flex;
606 align-items: center;
607 gap: 8px;
608 margin: 0;
609 font-size: 12.5px;
610 line-height: 1.4;
611 color: var( --presto-ink );
612 }
613
614 .presto-announce__feats svg {
615 flex: none;
616 color: var( --presto-blue );
617 }
618
619 /* CTA left, dismiss right. */
620 .presto-announce__actions {
621 display: flex;
622 align-items: center;
623 justify-content: space-between;
624 gap: 12px;
625 }
626
627 .presto-announce__close {
628 padding: 11px 2px;
629 border: 0;
630 background: none;
631 color: var( --presto-muted );
632 font-size: 13px;
633 line-height: 1;
634 cursor: pointer;
635 transition: color .16s ease;
636 }
637
638 .presto-announce__close:hover {
639 color: var( --presto-ink );
640 }
641
642 .presto-announce__cta {
643 display: inline-flex;
644 align-items: center;
645 gap: 7px;
646 padding: 11px 16px;
647 border: 0;
648 border-radius: 8px;
649 background: var( --presto-blue );
650 color: #fff;
651 font-size: 13px;
652 font-weight: 600;
653 line-height: 1;
654 text-decoration: none;
655 box-shadow: 0 6px 16px -8px rgba( 48, 88, 229, .7 );
656 transition: background-color .16s ease, box-shadow .16s ease, transform .16s ease;
657 }
658
659 .presto-announce__cta:hover,
660 .presto-announce__cta:focus {
661 background: #2246C9;
662 color: #fff;
663 box-shadow: 0 0 0 4px rgba( 48, 88, 229, .16 );
664 }
665
666 /* The arrow leans forward on hover — the only movement in the card. */
667 .presto-announce__cta:hover svg {
668 transform: translateX( 2px );
669 transition: transform .16s ease;
670 }
671
672 /* Corner close, over the artwork. */
673 .presto-announce__x {
674 position: absolute;
675 top: 12px;
676 right: 12px;
677 z-index: 2;
678 display: flex;
679 align-items: center;
680 justify-content: center;
681 width: 28px;
682 height: 28px;
683 padding: 0;
684 border: 0;
685 border-radius: 50%;
686 background: rgba( 255, 255, 255, .12 );
687 color: #fff;
688 cursor: pointer;
689 transition: background-color .16s ease;
690 }
691
692 .presto-announce__x:hover {
693 background: rgba( 255, 255, 255, .24 );
694 color: #fff;
695 }
696
697 .presto-announce__x:focus-visible,
698 .presto-announce__cta:focus-visible,
699 .presto-announce__close:focus-visible {
700 outline: 2px solid var( --presto-blue );
701 outline-offset: 3px;
702 }
703
704 @keyframes presto-announce-veil {
705 from { opacity: 0; }
706 }
707
708 @keyframes presto-announce-rise {
709 from { opacity: 0; transform: translateY( 14px ) scale( .98 ); }
710 }
711
712 @keyframes presto-announce-chat {
713 from { opacity: 0; transform: translateY( 10px ) scale( .97 ); }
714 }
715
716 @keyframes presto-announce-done {
717 from { opacity: 0; }
718 }
719
720 @keyframes presto-announce-row {
721 from { background-color: transparent; }
722 }
723
724 @media ( max-width: 600px ) {
725 .presto-announce__art { padding: 20px 18px 18px; }
726 .presto-announce__chat { width: 100%; margin-top: 12px; }
727 .presto-announce__body { padding: 20px 18px 18px; }
728 .presto-announce__title { font-size: 21px; }
729 }
730
731 @media ( prefers-reduced-motion: reduce ) {
732 .presto-announce,
733 .presto-announce__box,
734 .presto-announce__chat,
735 .presto-announce__chat-done,
736 .presto-announce__chips,
737 .presto-announce__list-new {
738 animation: none;
739 }
740 }
741 </style>
742 <?php
743 }
744
745 /**
746 * Close behaviour.
747 *
748 * @return void
749 */
750 protected function script() {
751 $args = array(
752 'action' => 'presto_dismiss_announcement',
753 '_wpnonce' => wp_create_nonce( self::NONCE_ACTION ),
754 );
755
756 $js = sprintf(
757 '( function() {
758 var modal = document.querySelector( ".presto-announce" );
759 if ( ! modal ) {
760 return;
761 }
762
763 function dismiss() {
764 return window.fetch( %1$s, {
765 method: "POST",
766 credentials: "same-origin",
767 body: new URLSearchParams( %2$s )
768 } );
769 }
770
771 function close() {
772 // The modal renders on the block-editor screen, where Escape is
773 // constant — a listener left bound after dismissal would fire an
774 // admin-ajax POST on every later press.
775 document.removeEventListener( "keydown", onKeydown );
776 modal.remove();
777 // A failed request only means the modal shows again.
778 dismiss();
779 }
780
781 function onKeydown( event ) {
782 if ( "Escape" === event.key ) {
783 close();
784 }
785 }
786
787 // aria-modal is a promise that focus is in here, so move it — onto
788 // the card itself, not the CTA, so nothing renders with a ring
789 // around it before the user has touched anything.
790 modal.querySelector( ".presto-announce__box" ).focus();
791
792 var cta = modal.querySelector( ".presto-announce__cta" );
793
794 // Wait for the dismiss to land before leaving, otherwise the next
795 // page renders the modal again before the request commits.
796 cta.addEventListener( "click", function( event ) {
797 event.preventDefault();
798 var href = this.href;
799 document.removeEventListener( "keydown", onKeydown );
800 modal.remove();
801 dismiss().catch( function() {} ).then( function() {
802 window.location.assign( href );
803 } );
804 } );
805
806 modal.querySelector( ".presto-announce__close" ).addEventListener( "click", close );
807 modal.querySelector( ".presto-announce__x" ).addEventListener( "click", close );
808
809 modal.addEventListener( "click", function( event ) {
810 if ( event.target === modal ) {
811 close();
812 }
813 } );
814 document.addEventListener( "keydown", onKeydown );
815 } )();',
816 wp_json_encode( admin_url( 'admin-ajax.php' ) ),
817 wp_json_encode( $args )
818 );
819
820 wp_print_inline_script_tag( $js );
821 }
822 }
823