| @@ -4,12 +4,14 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * Tracks which one-time announcements the current user has already |
| 6 | 6 | * dismissed, so each is shown once and never bothers them again. |
| 7 | 7 | * |
| 8 | - * Two surfaces use it today: the activation welcome dialog | |
| 8 | + * Three surfaces use it today: the activation welcome dialog | |
| 9 | 9 | * (`includes/welcome-dialog.php`, slug `activation-welcome`), shown |
| 10 | - * in the classic admin while OpenStation is disabled, and the rebrand | |
| 11 | - * notice (`src/rebrand-notice.ts`, slug `openstation-rebrand`). The | |
| 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 | |
| 12 | 14 | * key is intentionally generic, so anything else that needs |
| 13 | 15 | * show-once semantics registers its own slug and reuses this storage. |
| 14 | 16 | * OpenStation Preferences → Features exposes a "Reset what's-new |
| 15 | 17 | * dialogs" button that clears the whole list. |
| @@ -78,9 +80,10 @@ | ||
| 78 | 80 | /** |
| 79 | 81 | * Adds a slug to the user's seen-intros list. |
| 80 | 82 | * |
| 81 | 83 | * Idempotent — re-marking an already-seen intro is a no-op that |
| 82 | - * still returns true. | |
| 84 | + * still returns true. A slug that supersedes others | |
| 85 | + * ({@see openstation_seen_intros_superseded_by()}) removes them first. | |
| 83 | 86 | * |
| 84 | 87 | * @param int $user_id User ID. |
| 85 | 88 | * @param string $slug Intro slug. |
| 86 | 89 | * @return bool True on successful write (or no-op), false otherwise. |
| @@ -92,23 +95,47 @@ | ||
| 92 | 95 | return false; |
| 93 | 96 | } |
| 94 | 97 | |
| 95 | 98 | $current = openstation_get_seen_intros( $user_id ); |
| 96 | - if ( in_array( $slug, $current, true ) ) { | |
| 99 | + $kept = array_values( array_diff( $current, openstation_seen_intros_superseded_by( $slug ) ) ); | |
| 100 | + if ( in_array( $slug, $kept, true ) && count( $kept ) === count( $current ) ) { | |
| 97 | 101 | return true; |
| 98 | 102 | } |
| 99 | 103 | |
| 100 | - $current[] = $slug; | |
| 101 | - $current = array_slice( $current, 0, OPENSTATION_SEEN_INTROS_MAX ); | |
| 104 | + if ( ! in_array( $slug, $kept, true ) ) { | |
| 105 | + $kept[] = $slug; | |
| 106 | + } | |
| 107 | + $kept = array_slice( $kept, 0, OPENSTATION_SEEN_INTROS_MAX ); | |
| 102 | 108 | |
| 103 | 109 | return false !== update_user_meta( |
| 104 | 110 | $user_id, |
| 105 | 111 | OPENSTATION_SEEN_INTROS_META_KEY, |
| 106 | - $current | |
| 112 | + $kept | |
| 107 | 113 | ); |
| 108 | 114 | } |
| 109 | 115 | |
| 110 | 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 | +/** | |
| 111 | 138 | * Wipes every seen-intro entry for the user. Used by the OS |
| 112 | 139 | * Settings → Features "Reset what's-new dialogs" button. |
| 113 | 140 | * |
| 114 | 141 | * @param int $user_id User ID. |
| @@ -185,8 +212,31 @@ | ||
| 185 | 212 | } |
| 186 | 213 | add_action( 'rest_api_init', 'openstation_register_seen_intros_routes' ); |
| 187 | 214 | |
| 188 | 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 | +/** | |
| 189 | 239 | * Permission gate for the seen-intros routes. |
| 190 | 240 | * |
| 191 | 241 | * In-shell announcements (the rebrand notice, and anything a plugin |
| 192 | 242 | * registers) are only ever shown to a user who has already entered |
| @@ -193,17 +243,18 @@ | ||
| 193 | 243 | * OpenStation, so they keep the strict |
| 194 | 244 | * {@see openstation_rest_require_enabled()} gate — `read` alone is |
| 195 | 245 | * insufficient (every role, Subscriber included, carries `read`). |
| 196 | 246 | * |
| 197 | - * The one exception is the first-run welcome dialog | |
| 198 | - * ({@see OPENSTATION_WELCOME_INTRO_SLUG}): it renders in the *classic* | |
| 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* | |
| 199 | 250 | * admin precisely when OpenStation is NOT enabled, which is the only |
| 200 | - * state it ever appears in. Gating its dismissal behind | |
| 251 | + * state they ever appear in. Gating their dismissal behind | |
| 201 | 252 | * `openstation_rest_require_enabled()` would make the dismissal POST |
| 202 | 253 | * return 403 every time, so the slug could never be recorded as seen and |
| 203 | - * the dialog re-rendered on every classic-admin page load. We therefore | |
| 204 | - * let that single slug through for any logged-in `read`-capable account | |
| 205 | - * (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 | |
| 206 | 257 | * dismissal flag carries no privileged surface. The DELETE /intros route |
| 207 | 258 | * ("Reset what's-new dialogs") carries no slug and keeps the strict gate. |
| 208 | 259 | * |
| 209 | 260 | * @param WP_REST_Request $request The REST request. |
| @@ -210,9 +261,9 @@ | ||
| 210 | 261 | * @return true|WP_Error |
| 211 | 262 | */ |
| 212 | 263 | function openstation_rest_seen_intros_permission( WP_REST_Request $request ) { |
| 213 | 264 | $slug = sanitize_key( (string) $request->get_param( 'slug' ) ); |
| 214 | - if ( defined( 'OPENSTATION_WELCOME_INTRO_SLUG' ) && OPENSTATION_WELCOME_INTRO_SLUG === $slug ) { | |
| 265 | + if ( '' !== $slug && in_array( $slug, openstation_seen_intros_classic_admin_slugs(), true ) ) { | |
| 215 | 266 | if ( ! is_user_logged_in() ) { |
| 216 | 267 | return new WP_Error( |
| 217 | 268 | 'rest_forbidden', |
| 218 | 269 | __( 'Authentication required.', 'desktop-mode' ), |