PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 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 All 36 releases
← All changes | includes/seen-intros.php +123 -84 0.9.6 → 1.1.12 View file →
@@ -1,75 +1,81 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — "Seen intros" registry.
3 + * OpenStation — "Seen intros" registry.
4 4 *
5 - * Tracks which one-time introduction dialogs the current user has
6 - * already dismissed, so the shell can show a "what's new in this
7 - * native app" dialog the first time a ported native window opens
8 - * and never bother the user again afterwards.
5 + * Tracks which one-time announcements the current user has already
6 + * dismissed, so each is shown once and never bothers them again.
9 7 *
10 - * Today the surface is the native Posts window. The same key is
11 - * intentionally generic — any future ported native app (Pages,
12 - * Comments, Users, Plugins, …) registers its own slug and reuses
13 - * this storage. OS Settings → Features exposes a "Reset what's-new
14 - * dialogs" button that clears the whole list so the user can see
15 - * every intro again from scratch.
8 + * Three surfaces use it today: the activation welcome dialog
9 + * (`includes/welcome-dialog.php`, slug `activation-welcome`), shown
10 + * in the classic admin while OpenStation is disabled, the rebrand
11 + * notice (`src/rebrand-notice.ts`, slug `openstation-rebrand`), and
12 + * the usage feedback prompt (`includes/feedback/usage.php`, slug
13 + * `usage-feedback`, marked server-side on a successful send). The
14 + * key is intentionally generic, so anything else that needs
15 + * show-once semantics registers its own slug and reuses this storage.
16 + * OpenStation Preferences → Features exposes a "Reset what's-new
17 + * dialogs" button that clears the whole list.
16 18 *
17 19 * Storage shape:
18 20 * user meta `desktop_mode_seen_intros` → array<string> of slugs.
19 - * `[ 'posts' ]`, `[ 'posts', 'pages' ]`, etc. Slug values pass
20 - * through `sanitize_key()` and the list is capped at 64 entries
21 - * so a runaway client cannot bloat user-meta indefinitely.
21 + * `[ 'activation-welcome' ]`, `[ 'openstation-rebrand' ]`, etc.
22 + * Slug values pass through `sanitize_key()` and the list is capped
23 + * at 64 entries so a runaway client cannot bloat user-meta
24 + * indefinitely.
22 25 *
23 - * @package WPDesktopMode
24 - * @since 0.8.0
26 + * @package OpenStation
25 27 */
26 28
27 29 defined( 'ABSPATH' ) || exit;
28 30
29 -/** User meta key — see file header for shape. */
30 -const DESKTOP_MODE_SEEN_INTROS_META_KEY = 'desktop_mode_seen_intros';
31 +/**
32 + * User meta key — see file header for shape.
33 + *
34 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
35 + * persisted or externally-visible identifier, so renaming it would
36 + * orphan data already written by live installs (or break a live
37 + * URL). The mismatch between this constant's name and its value is
38 + * deliberate — it is NOT a half-finished rename.
39 + */
40 +const OPENSTATION_SEEN_INTROS_META_KEY = 'desktop_mode_seen_intros';
31 41
32 42 /** Hard cap so a malicious client cannot grow the list unbounded. */
33 -const DESKTOP_MODE_SEEN_INTROS_MAX = 64;
43 +const OPENSTATION_SEEN_INTROS_MAX = 64;
34 44
35 45 /**
36 46 * Returns the list of intro slugs the user has dismissed.
37 47 *
38 - * @since 0.8.0
39 - *
40 48 * @param int $user_id User ID.
41 49 * @return string[] Sanitized list (may be empty).
42 50 */
43 -function desktop_mode_get_seen_intros( $user_id ) {
51 +function openstation_get_seen_intros( $user_id ) {
44 52 $user_id = (int) $user_id;
45 53 if ( $user_id <= 0 ) {
46 54 return array();
47 55 }
48 56
49 - $raw = get_user_meta( $user_id, DESKTOP_MODE_SEEN_INTROS_META_KEY, true );
57 + $raw = get_user_meta( $user_id, OPENSTATION_SEEN_INTROS_META_KEY, true );
50 58 if ( ! is_array( $raw ) ) {
51 59 return array();
52 60 }
53 61
54 - return desktop_mode_sanitize_seen_intros( $raw );
62 + return openstation_sanitize_seen_intros( $raw );
55 63 }
56 64
57 65 /**
58 66 * Whether the user has already dismissed the given intro.
59 67 *
60 - * @since 0.8.0
61 - *
62 68 * @param int $user_id User ID.
63 69 * @param string $slug Intro slug (e.g. `'posts'`).
64 70 * @return bool
65 71 */
66 -function desktop_mode_has_seen_intro( $user_id, $slug ) {
72 +function openstation_has_seen_intro( $user_id, $slug ) {
67 73 $slug = sanitize_key( (string) $slug );
68 74 if ( '' === $slug ) {
69 75 return false;
70 76 }
71 - return in_array( $slug, desktop_mode_get_seen_intros( $user_id ), true );
77 + return in_array( $slug, openstation_get_seen_intros( $user_id ), true );
72 78 }
73 79
74 80 /**
75 81 * Adds a slug to the user's seen-intros list.
@@ -74,17 +80,16 @@
74 80 /**
75 81 * Adds a slug to the user's seen-intros list.
76 82 *
77 83 * Idempotent — re-marking an already-seen intro is a no-op that
78 - * still returns true.
84 + * still returns true. A slug that supersedes others
85 + * ({@see openstation_seen_intros_superseded_by()}) removes them first.
79 86 *
80 - * @since 0.8.0
81 - *
82 87 * @param int $user_id User ID.
83 88 * @param string $slug Intro slug.
84 89 * @return bool True on successful write (or no-op), false otherwise.
85 90 */
86 -function desktop_mode_mark_intro_seen( $user_id, $slug ) {
91 +function openstation_mark_intro_seen( $user_id, $slug ) {
87 92 $user_id = (int) $user_id;
88 93 $slug = sanitize_key( (string) $slug );
89 94 if ( $user_id <= 0 || '' === $slug ) {
90 95 return false;
@@ -89,49 +94,69 @@
89 94 if ( $user_id <= 0 || '' === $slug ) {
90 95 return false;
91 96 }
92 97
93 - $current = desktop_mode_get_seen_intros( $user_id );
94 - if ( in_array( $slug, $current, true ) ) {
98 + $current = openstation_get_seen_intros( $user_id );
99 + $kept = array_values( array_diff( $current, openstation_seen_intros_superseded_by( $slug ) ) );
100 + if ( in_array( $slug, $kept, true ) && count( $kept ) === count( $current ) ) {
95 101 return true;
96 102 }
97 103
98 - $current[] = $slug;
99 - $current = array_slice( $current, 0, DESKTOP_MODE_SEEN_INTROS_MAX );
104 + if ( ! in_array( $slug, $kept, true ) ) {
105 + $kept[] = $slug;
106 + }
107 + $kept = array_slice( $kept, 0, OPENSTATION_SEEN_INTROS_MAX );
100 108
101 109 return false !== update_user_meta(
102 110 $user_id,
103 - DESKTOP_MODE_SEEN_INTROS_META_KEY,
104 - $current
111 + OPENSTATION_SEEN_INTROS_META_KEY,
112 + $kept
105 113 );
106 114 }
107 115
108 116 /**
117 + * Slugs a newly recorded one makes obsolete: facts where only the
118 + * latest counts, which an append-only list cannot otherwise express.
119 + *
120 + * The shell tour's two outcomes are the one pair. A run ends skipped
121 + * or finished, and the relaunch icon asks about the LATEST run: kept
122 + * side by side, one finished run long ago hid the icon after every
123 + * skip that came later. The strings mirror the constants in
124 + * `includes/first-run/shell-tour.php`, which loads after this file.
125 + *
126 + * @param string $slug The slug being recorded.
127 + * @return string[] Slugs it replaces.
128 + */
129 +function openstation_seen_intros_superseded_by( $slug ) {
130 + $pairs = array(
131 + 'shell-tour-skipped' => array( 'shell-tour-done' ),
132 + 'shell-tour-done' => array( 'shell-tour-skipped' ),
133 + );
134 + return isset( $pairs[ $slug ] ) ? $pairs[ $slug ] : array();
135 +}
136 +
137 +/**
109 138 * Wipes every seen-intro entry for the user. Used by the OS
110 139 * Settings → Features "Reset what's-new dialogs" button.
111 140 *
112 - * @since 0.8.0
113 - *
114 141 * @param int $user_id User ID.
115 142 * @return bool True on success.
116 143 */
117 -function desktop_mode_clear_seen_intros( $user_id ) {
144 +function openstation_clear_seen_intros( $user_id ) {
118 145 $user_id = (int) $user_id;
119 146 if ( $user_id <= 0 ) {
120 147 return false;
121 148 }
122 - return (bool) delete_user_meta( $user_id, DESKTOP_MODE_SEEN_INTROS_META_KEY );
149 + return (bool) delete_user_meta( $user_id, OPENSTATION_SEEN_INTROS_META_KEY );
123 150 }
124 151
125 152 /**
126 153 * Coerces a raw payload to a clean list of slugs.
127 154 *
128 - * @since 0.8.0
129 - *
130 155 * @param mixed $raw Raw value.
131 156 * @return string[]
132 157 */
133 -function desktop_mode_sanitize_seen_intros( $raw ) {
158 +function openstation_sanitize_seen_intros( $raw ) {
134 159 if ( ! is_array( $raw ) ) {
135 160 return array();
136 161 }
137 162 $out = array();
@@ -144,9 +169,9 @@
144 169 continue;
145 170 }
146 171 $out[] = $slug;
147 172 }
148 - return array_slice( array_values( array_unique( $out ) ), 0, DESKTOP_MODE_SEEN_INTROS_MAX );
173 + return array_slice( array_values( array_unique( $out ) ), 0, OPENSTATION_SEEN_INTROS_MAX );
149 174 }
150 175
151 176 /**
152 177 * Registers REST routes for the seen-intros surface.
@@ -156,19 +181,17 @@
156 181 * DELETE /desktop-mode/v1/intros no body — clears the list
157 182 *
158 183 * Both return the post-mutation list so the client can refresh its
159 184 * local snapshot without a follow-up GET.
160 - *
161 - * @since 0.8.0
162 185 */
163 -function desktop_mode_register_seen_intros_routes() {
186 +function openstation_register_seen_intros_routes() {
164 187 register_rest_route(
165 188 'desktop-mode/v1',
166 189 '/intros/seen',
167 190 array(
168 191 'methods' => WP_REST_Server::CREATABLE,
169 - 'callback' => 'desktop_mode_rest_mark_intro_seen',
170 - 'permission_callback' => 'desktop_mode_rest_seen_intros_permission',
192 + 'callback' => 'openstation_rest_mark_intro_seen',
193 + 'permission_callback' => 'openstation_rest_seen_intros_permission',
171 194 'args' => array(
172 195 'slug' => array(
173 196 'required' => true,
174 197 'type' => 'string',
@@ -181,46 +204,66 @@
181 204 'desktop-mode/v1',
182 205 '/intros',
183 206 array(
184 207 'methods' => WP_REST_Server::DELETABLE,
185 - 'callback' => 'desktop_mode_rest_clear_seen_intros',
186 - 'permission_callback' => 'desktop_mode_rest_seen_intros_permission',
208 + 'callback' => 'openstation_rest_clear_seen_intros',
209 + 'permission_callback' => 'openstation_rest_seen_intros_permission',
187 210 )
188 211 );
189 212 }
190 -add_action( 'rest_api_init', 'desktop_mode_register_seen_intros_routes' );
213 +add_action( 'rest_api_init', 'openstation_register_seen_intros_routes' );
191 214
192 215 /**
216 + * The intro slugs whose dismissal is accepted from an account that has
217 + * NOT enabled OpenStation.
218 + *
219 + * Exactly the intros that render in the classic admin while the shell
220 + * is off: the welcome dialog and the activation nudge. Everything else
221 + * is shown inside the shell and keeps the strict gate. Adding a slug
222 + * here is adding a classic-admin surface; the allowlist is the review
223 + * point, so keep it a literal list.
224 + *
225 + * @return string[]
226 + */
227 +function openstation_seen_intros_classic_admin_slugs() {
228 + $slugs = array();
229 + if ( defined( 'OPENSTATION_WELCOME_INTRO_SLUG' ) ) {
230 + $slugs[] = OPENSTATION_WELCOME_INTRO_SLUG;
231 + }
232 + if ( defined( 'OPENSTATION_ACTIVATION_NUDGE_INTRO_SLUG' ) ) {
233 + $slugs[] = OPENSTATION_ACTIVATION_NUDGE_INTRO_SLUG;
234 + }
235 + return $slugs;
236 +}
237 +
238 +/**
193 239 * Permission gate for the seen-intros routes.
194 240 *
195 - * In-shell intros (slug `posts`, `pages`, …) are only ever shown to a
196 - * user who has already entered Desktop Mode, so they keep the strict
197 - * {@see desktop_mode_rest_require_enabled()} gate — `read` alone is
241 + * In-shell announcements (the rebrand notice, and anything a plugin
242 + * registers) are only ever shown to a user who has already entered
243 + * OpenStation, so they keep the strict
244 + * {@see openstation_rest_require_enabled()} gate — `read` alone is
198 245 * insufficient (every role, Subscriber included, carries `read`).
199 246 *
200 - * The one exception is the first-run welcome dialog
201 - * ({@see DESKTOP_MODE_WELCOME_INTRO_SLUG}): it renders in the *classic*
202 - * admin precisely when Desktop Mode is NOT enabled, which is the only
203 - * state it ever appears in. Gating its dismissal behind
204 - * `desktop_mode_rest_require_enabled()` would make the dismissal POST
247 + * The exceptions are the classic-admin intros
248 + * ({@see openstation_seen_intros_classic_admin_slugs()}): the first-run
249 + * welcome dialog and the activation nudge both render in the *classic*
250 + * admin precisely when OpenStation is NOT enabled, which is the only
251 + * state they ever appear in. Gating their dismissal behind
252 + * `openstation_rest_require_enabled()` would make the dismissal POST
205 253 * return 403 every time, so the slug could never be recorded as seen and
206 - * the dialog re-rendered on every classic-admin page load. We therefore
207 - * let that single slug through for any logged-in `read`-capable account
208 - * (the exact audience the dialog is shown to); writing one's own
254 + * the dialog / notice re-rendered on every classic-admin page load. We
255 + * therefore let those slugs through for any logged-in `read`-capable
256 + * account (the exact audience they are shown to); writing one's own
209 257 * dismissal flag carries no privileged surface. The DELETE /intros route
210 258 * ("Reset what's-new dialogs") carries no slug and keeps the strict gate.
211 259 *
212 - * @since 0.8.0
213 - * @since 0.8.10 Hardened to require desktop mode enabled (was `read`).
214 - * @since 0.30.1 Allow the `activation-welcome` slug without the enabled
215 - * gate, so the welcome dialog's dismissal can persist.
216 - *
217 260 * @param WP_REST_Request $request The REST request.
218 261 * @return true|WP_Error
219 262 */
220 -function desktop_mode_rest_seen_intros_permission( WP_REST_Request $request ) {
263 +function openstation_rest_seen_intros_permission( WP_REST_Request $request ) {
221 264 $slug = sanitize_key( (string) $request->get_param( 'slug' ) );
222 - if ( defined( 'DESKTOP_MODE_WELCOME_INTRO_SLUG' ) && DESKTOP_MODE_WELCOME_INTRO_SLUG === $slug ) {
265 + if ( '' !== $slug && in_array( $slug, openstation_seen_intros_classic_admin_slugs(), true ) ) {
223 266 if ( ! is_user_logged_in() ) {
224 267 return new WP_Error(
225 268 'rest_forbidden',
226 269 __( 'Authentication required.', 'desktop-mode' ),
@@ -236,32 +279,30 @@
236 279 }
237 280 return true;
238 281 }
239 282
240 - return desktop_mode_rest_require_enabled();
283 + return openstation_rest_require_enabled();
241 284 }
242 285
243 286 /**
244 287 * REST handler for `POST /desktop-mode/v1/intros/seen`.
245 288 *
246 - * @since 0.8.0
247 - *
248 289 * @param WP_REST_Request $request REST request.
249 290 * @return WP_REST_Response|WP_Error
250 291 */
251 -function desktop_mode_rest_mark_intro_seen( WP_REST_Request $request ) {
292 +function openstation_rest_mark_intro_seen( WP_REST_Request $request ) {
252 293 $user_id = get_current_user_id();
253 294 $slug = sanitize_key( (string) $request->get_param( 'slug' ) );
254 295 if ( '' === $slug ) {
255 296 return new WP_Error(
256 - 'desktop_mode_invalid_intro_slug',
297 + 'openstation_invalid_intro_slug',
257 298 __( 'The `slug` parameter must be a non-empty string.', 'desktop-mode' ),
258 299 array( 'status' => 400 )
259 300 );
260 301 }
261 - desktop_mode_mark_intro_seen( $user_id, $slug );
302 + openstation_mark_intro_seen( $user_id, $slug );
262 303 return rest_ensure_response(
263 - array( 'seenIntros' => desktop_mode_get_seen_intros( $user_id ) )
304 + array( 'seenIntros' => openstation_get_seen_intros( $user_id ) )
264 305 );
265 306 }
266 307
267 308 /**
@@ -266,15 +307,13 @@
266 307
267 308 /**
268 309 * REST handler for `DELETE /desktop-mode/v1/intros`.
269 310 *
270 - * @since 0.8.0
271 - *
272 311 * @return WP_REST_Response
273 312 */
274 -function desktop_mode_rest_clear_seen_intros() {
313 +function openstation_rest_clear_seen_intros() {
275 314 $user_id = get_current_user_id();
276 - desktop_mode_clear_seen_intros( $user_id );
315 + openstation_clear_seen_intros( $user_id );
277 316 return rest_ensure_response(
278 - array( 'seenIntros' => desktop_mode_get_seen_intros( $user_id ) )
317 + array( 'seenIntros' => openstation_get_seen_intros( $user_id ) )
279 318 );
280 319 }