PluginProbe
wpForo Forum / 3.0.8
wpForo Forum v3.0.8
3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.2 All 137 releases
wpforo / classes / FeatureIntro.php

FeatureIntro.php in wpForo Forum 3.0.8, at classes/FeatureIntro.php

295 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace wpforo\classes;
4
5 // Exit if accessed directly
6 if( ! defined( 'ABSPATH' ) ) exit;
7
8 class FeatureIntro {
9
10 /**
11 * Current intro version - bump this when adding new feature intro content.
12 * Must match a key in get_intros().
13 *
14 * Add a new entry in get_intros():
15 *
16 * '3.0.0' => [ 'slides' => [ ... ] ], // existing
17 * '3.1.0' => [ 'slides' => [ ... ] ], // new
18 * Update the constant:
19 *
20 * const CURRENT_INTRO = '3.1.0';
21 */
22 const CURRENT_INTRO = '3.0.0';
23
24 const META_KEY = 'wpforo_feature_intro_dismissed';
25 const OPT_OUT_KEY = 'wpforo_feature_intro_opt_out';
26 const VERSION_OPTION = 'wpforo_feature_intro_version';
27
28 private $intros = [];
29
30 public function __construct() {
31 $this->intros = $this->get_intros();
32
33 add_action( 'admin_enqueue_scripts', [ $this, 'maybe_enqueue_assets' ] );
34 add_action( 'admin_footer', [ $this, 'maybe_render_modal' ] );
35 }
36
37 /**
38 * Define all feature introductions.
39 * To add a new intro for a future version, add a new key here.
40 */
41 private function get_intros() {
42 return [
43 '3.0.0' => [
44 'slides' => [
45 [
46 'title' => __( "What's New in wpForo 3.0 - AI Edition", 'wpforo' ),
47 'description' => __( 'A major update with a reimagined modern AI-driven discussion system.', 'wpforo' ),
48 'image' => 'whats-new-page.png',
49 'features' => [
50 __( '"What\' New" page. Timeline-based activity feed showing all forum actions: new topics, replies, likes, dislikes and more...', 'wpforo' ),
51 __( 'Refreshed new theme with the brand new Boxed Layout for a clean, card-based forum experience.', 'wpforo' ),
52 __( 'Using Forum as Support Tickets Board. Turns any forum into a private support ticket system.', 'wpforo' ),
53 __( '99.9% Spam Protection with New user control, AI antispam, flood detection and reCAPTCHA v3.', 'wpforo' ),
54 __( 'Block Widgets replacing the legacy widgets.', 'wpforo' ),
55 __( '360° powered AI features shown next &#8594;', 'wpforo' ),
56 ],
57 ],
58 [
59 'title' => __( 'New Boxed Forum Layout', 'wpforo' ),
60 'description' => __( 'The fifth forum layout with a modern boxed card design. All five layouts have been refreshed with a clean, contemporary look.', 'wpforo' ),
61 'image' => 'boxed-forum-layout.png',
62 'tip' => __( 'Go to <strong>wpForo &rarr; Forums</strong> to change the layout of any forum.', 'wpforo' ),
63 ],
64 [
65 'title' => __( '360° AI-Powered Features', 'wpforo' ),
66 'description' => __( '20+ AI Features are added! wpForo 3.0 introduces revolutionary AI features that make your forum smarter, safer, and more engaging — while keeping you in full control of every feature.', 'wpforo' ),
67 'image' => 'ai-search-widget.png',
68 'features' => [
69 __( 'AI Semantic Search, finds topics by meaning', 'wpforo' ),
70 __( 'AI Translation, 100+ languages, real-time', 'wpforo' ),
71 __( 'AI Topic Summary, instant discussion overviews', 'wpforo' ),
72 __( 'AI Chat Assistant, 24/7 forum chatbot', 'wpforo' ),
73 __( 'AI Content Moderation, spam and toxicity detection.', 'wpforo' ),
74 __( 'AI Topic Suggestions, smart recommendations', 'wpforo' ),
75 ],
76 ],
77 [
78 'title' => __( 'Connect to AI Service', 'wpforo' ),
79 'description' => __( 'Get started with one click &mdash; no configuration needed.', 'wpforo' ),
80 'image' => 'connect-to-service.png',
81 'steps' => [
82 __( 'Go to <strong>wpForo &rarr; AI Features</strong>', 'wpforo' ),
83 __( 'Check the Terms of Service checkbox', 'wpforo' ),
84 __( 'Click <strong>"Generate API Key & Connect"</strong>', 'wpforo' ),
85 ],
86 'highlight' => __( 'Get started in one click for FREE!', 'wpforo' ),
87 ],
88 [
89 'title' => __( 'You\'re in Control', 'wpforo' ),
90 'description' => __( 'Every AI feature can be independently enabled or disabled. Only spend credits on what you use. Start with just search and add more features anytime.', 'wpforo' ),
91 'image' => 'ai-settings.png',
92 'tip' => __( 'Go to <strong>wpForo &rarr; Settings &rarr; AI Features</strong> to toggle each feature on or off.', 'wpforo' ),
93 ],
94 ],
95 ],
96 ];
97 }
98
99 /**
100 * Should the intro modal be shown to the current user?
101 */
102 public function should_show() {
103 if( ! current_user_can( 'manage_options' ) ) return false;
104 if( ! $this->is_wpforo_admin_page() ) return false;
105 if( $this->is_user_opted_out() ) return false;
106 if( $this->is_dismissed( self::CURRENT_INTRO ) ) return false;
107 if( ! isset( $this->intros[ self::CURRENT_INTRO ] ) ) return false;
108
109 return true;
110 }
111
112 /**
113 * Check if we're on a wpForo admin page.
114 */
115 private function is_wpforo_admin_page() {
116 if( ! function_exists( 'get_current_screen' ) ) return false;
117
118 $screen = get_current_screen();
119 if( ! $screen ) return false;
120
121 return strpos( $screen->id, 'wpforo' ) !== false;
122 }
123
124 /**
125 * Check if user permanently opted out of all intros.
126 */
127 private function is_user_opted_out() {
128 return (bool) get_user_meta( get_current_user_id(), self::OPT_OUT_KEY, true );
129 }
130
131 /**
132 * Check if user dismissed a specific intro version.
133 */
134 private function is_dismissed( $version ) {
135 $dismissed = get_user_meta( get_current_user_id(), self::META_KEY, true );
136 if( ! is_array( $dismissed ) ) $dismissed = [];
137
138 return isset( $dismissed[ $version ] );
139 }
140
141 /**
142 * Enqueue CSS and JS for the intro modal.
143 */
144 public function maybe_enqueue_assets() {
145 if( ! $this->should_show() ) return;
146
147 wp_enqueue_style(
148 'wpforo-feature-intro',
149 WPFORO_URL . '/admin/assets/css/feature-intro.css',
150 [],
151 WPFORO_VERSION
152 );
153
154 wp_enqueue_script(
155 'wpforo-feature-intro',
156 WPFORO_URL . '/admin/assets/js/feature-intro.js',
157 [ 'jquery' ],
158 WPFORO_VERSION,
159 true
160 );
161
162 wp_localize_script( 'wpforo-feature-intro', 'wpforoFeatureIntro', [
163 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
164 'nonce' => wp_create_nonce( 'wpforo_feature_intro' ),
165 'version' => self::CURRENT_INTRO,
166 'connectUrl' => admin_url( 'admin.php?page=wpforo-ai' ),
167 'totalSlides' => count( $this->intros[ self::CURRENT_INTRO ]['slides'] ),
168 ] );
169 }
170
171 /**
172 * Render the modal HTML in admin footer.
173 */
174 public function maybe_render_modal() {
175 if( ! $this->should_show() ) return;
176
177 $intro = $this->intros[ self::CURRENT_INTRO ];
178 $slides = $intro['slides'];
179 $img_url = WPFORO_URL . '/assets/images/intro/';
180 ?>
181 <div id="wpforo-feature-intro-overlay" class="wpf-fi-overlay">
182 <div class="wpf-fi-modal">
183 <button type="button" class="wpf-fi-close" title="<?php esc_attr_e( 'Close', 'wpforo' ); ?>">&times;</button>
184
185 <div class="wpf-fi-slides-wrap">
186 <?php foreach( $slides as $i => $slide ) : ?>
187 <div class="wpf-fi-slide<?php echo $i === 0 ? ' wpf-fi-active' : ''; ?>" data-slide="<?php echo (int) $i; ?>">
188
189 <?php if( ! empty( $slide['image'] ) ) : ?>
190 <div class="wpf-fi-image-wrap">
191 <img src="<?php echo esc_url( $img_url . $slide['image'] ); ?>"
192 alt="<?php echo esc_attr( wp_strip_all_tags( $slide['title'] ) ); ?>" />
193 </div>
194 <?php endif; ?>
195
196 <div class="wpf-fi-content">
197 <h2 class="wpf-fi-title"><?php echo esc_html( $slide['title'] ); ?></h2>
198 <p class="wpf-fi-desc"><?php echo wp_kses_post( $slide['description'] ); ?></p>
199
200 <?php if( ! empty( $slide['features'] ) ) : ?>
201 <ul class="wpf-fi-features">
202 <?php foreach( $slide['features'] as $feature ) : ?>
203 <li><?php echo wp_kses_post( $feature ); ?></li>
204 <?php endforeach; ?>
205 </ul>
206 <?php endif; ?>
207
208 <?php if( ! empty( $slide['steps'] ) ) : ?>
209 <ol class="wpf-fi-steps">
210 <?php foreach( $slide['steps'] as $step ) : ?>
211 <li><?php echo wp_kses_post( $step ); ?></li>
212 <?php endforeach; ?>
213 </ol>
214 <?php endif; ?>
215
216 <?php if( ! empty( $slide['highlight'] ) ) : ?>
217 <div class="wpf-fi-highlight"><?php echo wp_kses_post( $slide['highlight'] ); ?></div>
218 <?php endif; ?>
219
220 <?php if( ! empty( $slide['tip'] ) ) : ?>
221 <div class="wpf-fi-tip"><?php echo wp_kses_post( $slide['tip'] ); ?></div>
222 <?php endif; ?>
223 </div>
224 </div>
225 <?php endforeach; ?>
226 </div>
227
228 <div class="wpf-fi-nav">
229 <button type="button" class="wpf-fi-arrow wpf-fi-prev" title="<?php esc_attr_e( 'Previous', 'wpforo' ); ?>">&lsaquo;</button>
230 <div class="wpf-fi-dots">
231 <?php for( $i = 0; $i < count( $slides ); $i++ ) : ?>
232 <span class="wpf-fi-dot<?php echo $i === 0 ? ' wpf-fi-active' : ''; ?>" data-slide="<?php echo (int) $i; ?>"></span>
233 <?php endfor; ?>
234 </div>
235 <button type="button" class="wpf-fi-arrow wpf-fi-next" title="<?php esc_attr_e( 'Next', 'wpforo' ); ?>">&rsaquo;</button>
236 </div>
237
238 <div class="wpf-fi-actions">
239 <div class="wpf-fi-actions-left">
240 <label class="wpf-fi-opt-out">
241 <input type="checkbox" id="wpf-fi-opt-out" />
242 <?php _e( "Don't show introductions for new features", 'wpforo' ); ?>
243 </label>
244 </div>
245 <div class="wpf-fi-actions-right">
246 <button type="button" class="wpf-fi-btn wpf-fi-btn-secondary wpf-fi-dismiss">
247 <?php _e( 'Maybe Later', 'wpforo' ); ?>
248 </button>
249 <a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforo-ai' ) ); ?>" class="wpf-fi-btn wpf-fi-btn-primary wpf-fi-connect">
250 <?php _e( 'Activate AI Features', 'wpforo' ); ?> &rarr;
251 </a>
252 </div>
253 </div>
254 </div>
255 </div>
256 <?php
257 }
258
259 /**
260 * AJAX handler: dismiss the intro for the current user.
261 * Instance method (called when class is instantiated in admin context).
262 */
263 public function ajax_dismiss() {
264 self::ajax_dismiss_static();
265 }
266
267 /**
268 * Static AJAX handler: dismiss the intro for the current user.
269 * Called from includes/hooks.php during AJAX requests (admin/index.php is not loaded for AJAX).
270 */
271 public static function ajax_dismiss_static() {
272 check_ajax_referer( 'wpforo_feature_intro', '_wpnonce' );
273
274 if( ! current_user_can( 'manage_options' ) ) {
275 wp_send_json_error( 'Unauthorized', 403 );
276 }
277
278 $version = isset( $_POST['version'] ) ? sanitize_text_field( $_POST['version'] ) : self::CURRENT_INTRO;
279 $opt_out = isset( $_POST['opt_out'] ) && $_POST['opt_out'] === '1';
280
281 // Mark this version as dismissed
282 $dismissed = get_user_meta( get_current_user_id(), self::META_KEY, true );
283 if( ! is_array( $dismissed ) ) $dismissed = [];
284 $dismissed[ $version ] = time();
285 update_user_meta( get_current_user_id(), self::META_KEY, $dismissed );
286
287 // Handle permanent opt-out
288 if( $opt_out ) {
289 update_user_meta( get_current_user_id(), self::OPT_OUT_KEY, true );
290 }
291
292 wp_send_json_success();
293 }
294 }
295