PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.2
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.2
4.0.2 4.0.1 4.0.0 3.16.6 3.16.5 3.16.4 3.16.3 3.16.2 3.16.1 3.16.0 3.15.9 3.9.9 3.9.5 3.9.6 3.9.7 3.9.8 1.1.7 1.1.8 1.1.9 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 All 340 releases
profile-builder / admin / support-chat.php

support-chat.php in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.2, at admin/support-chat.php

441 lines 16.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Support Chat Widget
4 *
5 * Displays a chat-like popup showing recent WordPress.org forum topics
6 * and encourages users to open support tickets.
7 *
8 * @package ProfileBuilder
9 * @since 3.15.4
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) exit;
13
14 /**
15 * Class WPPB_Support_Chat
16 *
17 * Handles the support chat widget functionality
18 */
19 class WPPB_Support_Chat {
20
21 /**
22 * RSS Feed URL for the plugin support forum
23 */
24 const FEED_URL = 'https://wordpress.org/support/plugin/profile-builder/feed/';
25
26 /**
27 * Support forum URL
28 */
29 const FORUM_URL = 'https://wordpress.org/support/plugin/profile-builder/';
30
31 /**
32 * New topic URL
33 */
34 const NEW_TOPIC_URL = 'https://wordpress.org/support/plugin/profile-builder/#new-topic-0';
35
36 /**
37 * Transient key for caching forum posts
38 */
39 const CACHE_KEY = 'wppb_support_forum_posts';
40
41 /**
42 * Cache duration in seconds (1 hour)
43 */
44 const CACHE_DURATION = HOUR_IN_SECONDS;
45
46 /**
47 * Number of posts to display
48 */
49 const POSTS_COUNT = 5;
50
51 /**
52 * User meta key for last viewed timestamp
53 */
54 const LAST_VIEWED_META_KEY = 'wppb_support_chat_last_viewed';
55
56 /**
57 * Instance
58 */
59 private static $instance = null;
60
61 /**
62 * Get instance
63 */
64 public static function get_instance() {
65 if ( null === self::$instance ) {
66 self::$instance = new self();
67 }
68 return self::$instance;
69 }
70
71 /**
72 * Constructor
73 */
74 private function __construct() {
75
76 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
77 add_action( 'admin_footer', array( $this, 'render_chat_widget' ) );
78 add_action( 'wp_ajax_wppb_get_forum_posts', array( $this, 'ajax_get_forum_posts' ) );
79 add_action( 'wp_ajax_wppb_mark_forum_posts_read', array( $this, 'ajax_mark_forum_posts_read' ) );
80 }
81
82 /**
83 * Check if we should show the chat widget
84 *
85 * @return bool
86 */
87 private function should_show_widget() {
88 // Only show on Profile Builder admin pages
89 return $this->is_profile_builder_page();
90 }
91
92 /**
93 * Check if current page is a Profile Builder admin page
94 *
95 * @return bool
96 */
97 private function is_profile_builder_page() {
98 if ( ! is_admin() ) {
99 return false;
100 }
101
102 $screen = get_current_screen();
103 if ( ! $screen ) {
104 return false;
105 }
106
107 // Check for PB pages
108 $pb_pages = array(
109 'profile-builder',
110 'profile-builder-dashboard',
111 'profile-builder-basic-info',
112 'profile-builder-general-settings',
113 'profile-builder-add-ons',
114 'manage-fields',
115 );
116
117 foreach ( $pb_pages as $page ) {
118 if ( strpos( $screen->id, $page ) !== false ) {
119 return true;
120 }
121 }
122
123 // Include PB form/userlisting CPT admin screens.
124 if ( ! empty( $screen->post_type ) ) {
125 $pb_cpt_pages = array( 'wppb-ul-cpt', 'wppb-rf-cpt', 'wppb-epf-cpt' );
126 if ( in_array( $screen->post_type, $pb_cpt_pages, true ) ) {
127 return true;
128 }
129 }
130
131 return false;
132 }
133
134 /**
135 * Enqueue assets
136 */
137 public function enqueue_assets() {
138 if ( ! $this->should_show_widget() ) {
139 return;
140 }
141
142 wp_enqueue_style(
143 'wppb-support-chat',
144 WPPB_PLUGIN_URL . 'assets/css/wppb-support-chat.css',
145 array(),
146 PROFILE_BUILDER_VERSION
147 );
148
149 wp_enqueue_script(
150 'wppb-support-chat',
151 WPPB_PLUGIN_URL . 'assets/js/wppb-support-chat.js',
152 array(),
153 PROFILE_BUILDER_VERSION,
154 true
155 );
156
157 // Compute new post count from cached data, fetching if needed on first visit.
158 $new_count = 0;
159 $last_viewed = (int) get_user_meta( get_current_user_id(), self::LAST_VIEWED_META_KEY, true );
160 $cached_posts = get_transient( self::CACHE_KEY );
161 if ( false === $cached_posts ) {
162 $cached_posts = $this->get_forum_posts();
163 }
164 if ( is_array( $cached_posts ) ) {
165 foreach ( $cached_posts as $post ) {
166 if ( ! empty( $post['timestamp'] ) && $post['timestamp'] > $last_viewed ) {
167 $new_count++;
168 }
169 }
170 }
171
172 wp_localize_script( 'wppb-support-chat', 'wppbSupportChat', array(
173 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
174 'nonce' => wp_create_nonce( 'wppb_support_chat' ),
175 'forumUrl' => self::FORUM_URL,
176 'newTopicUrl' => self::NEW_TOPIC_URL,
177 'newCount' => $new_count,
178 'lastViewed' => $last_viewed,
179 'strings' => array(
180 'title' => __( 'Need Help?', 'profile-builder' ),
181 'subtitle' => __( 'Recent community discussions', 'profile-builder' ),
182 'loading' => __( 'Loading...', 'profile-builder' ),
183 'error' => __( 'Unable to load forum posts', 'profile-builder' ),
184 'askQuestion' => __( 'Ask a Question', 'profile-builder' ),
185 'viewAll' => __( 'View All Topics', 'profile-builder' ),
186 'postedBy' => __( 'by', 'profile-builder' ),
187 'encourageTitle' => __( 'Have a question?', 'profile-builder' ),
188 'encourageText' => __( 'Get help directly from the plugin developers, suggest improvements, or share your feedback!', 'profile-builder' ),
189 'tipTitle' => __( 'Tip for faster help:', 'profile-builder' ),
190 'tipText' => __( 'Include what you tried, what you expected, and what happened. Screenshots help!', 'profile-builder' ),
191 ),
192 ) );
193 }
194
195 /**
196 * Render chat widget HTML
197 */
198 public function render_chat_widget() {
199 if ( ! $this->should_show_widget() ) {
200 return;
201 }
202 ?>
203 <div id="wppb-support-chat-widget" class="wppb-support-chat" style="display: none;">
204 <!-- Chat Toggle Button - Bar style -->
205 <button type="button" class="wppb-support-chat__toggle" aria-label="<?php esc_attr_e( 'Toggle support chat', 'profile-builder' ); ?>">
206 <span class="wppb-support-chat__toggle-content">
207 <span class="wppb-support-chat__toggle-icon wppb-support-chat__toggle-icon--chat">
208 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
209 <path d="M12 2C6.48 2 2 6.04 2 11c0 2.21.89 4.22 2.34 5.75L2 22l5.25-2.34C8.78 20.53 10.35 21 12 21c5.52 0 10-4.04 10-9s-4.48-9-10-9zm0 16c-1.34 0-2.62-.29-3.78-.82l-.37-.18-2.49 1.11.98-2.58-.28-.4C4.74 13.98 4 12.55 4 11c0-3.87 3.59-7 8-7s8 3.13 8 7-3.59 7-8 7z"/>
210 <circle cx="8" cy="11" r="1.5"/>
211 <circle cx="12" cy="11" r="1.5"/>
212 <circle cx="16" cy="11" r="1.5"/>
213 </svg>
214 </span>
215 <span class="wppb-support-chat__toggle-icon wppb-support-chat__toggle-icon--close">
216 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
217 <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
218 </svg>
219 </span>
220 <span class="wppb-support-chat__toggle-text">
221 <?php esc_html_e( 'Need Help?', 'profile-builder' ); ?>
222 <span class="wppb-support-chat__toggle-subtext"><?php esc_html_e( 'Ask the community', 'profile-builder' ); ?></span>
223 </span>
224 </span>
225 <span class="wppb-support-chat__badge"></span>
226 </button>
227
228 <!-- Chat Window -->
229 <div class="wppb-support-chat__window">
230 <!-- Header -->
231 <div class="wppb-support-chat__header">
232 <div class="wppb-support-chat__header-content">
233 <div class="wppb-support-chat__avatar">
234 <img src="<?php echo esc_url( WPPB_PLUGIN_URL . 'assets/images/pb-logo.svg' ); ?>" alt="Profile Builder" width="32" height="32">
235 </div>
236 <div class="wppb-support-chat__header-text">
237 <h4 class="wppb-support-chat__title"></h4>
238 <p class="wppb-support-chat__subtitle"></p>
239 </div>
240 </div>
241 <button type="button" class="wppb-support-chat__close" aria-label="<?php esc_attr_e( 'Close', 'profile-builder' ); ?>">
242 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
243 <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
244 </svg>
245 </button>
246 </div>
247
248 <!-- Body -->
249 <div class="wppb-support-chat__body">
250 <!-- Encourage section - at top -->
251 <div class="wppb-support-chat__encourage">
252 <div class="wppb-support-chat__encourage-card">
253 <h5 class="wppb-support-chat__encourage-title"></h5>
254 <p class="wppb-support-chat__encourage-text"></p>
255 </div>
256 <div class="wppb-support-chat__tip">
257 <strong class="wppb-support-chat__tip-title"></strong>
258 <p class="wppb-support-chat__tip-text"></p>
259 </div>
260 </div>
261
262 <!-- Recent discussions label -->
263 <div class="wppb-support-chat__section-label"></div>
264
265 <!-- Loading state -->
266 <div class="wppb-support-chat__loading">
267 <div class="wppb-support-chat__spinner"></div>
268 <span></span>
269 </div>
270
271 <!-- Posts list -->
272 <div class="wppb-support-chat__posts"></div>
273 </div>
274
275 <!-- Footer -->
276 <div class="wppb-support-chat__footer">
277 <a href="<?php echo esc_url( self::NEW_TOPIC_URL ); ?>" target="_blank" rel="noopener" class="wppb-support-chat__btn wppb-support-chat__btn--primary">
278 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
279 <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
280 </svg>
281 <span></span>
282 </a>
283 <a href="<?php echo esc_url( self::FORUM_URL ); ?>" target="_blank" rel="noopener" class="wppb-support-chat__btn wppb-support-chat__btn--secondary">
284 <span></span>
285 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
286 <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
287 </svg>
288 </a>
289 </div>
290 </div>
291 </div>
292 <?php
293 }
294
295 /**
296 * AJAX handler to get forum posts
297 */
298 public function ajax_get_forum_posts() {
299 check_ajax_referer( 'wppb_support_chat', 'nonce' );
300
301 if ( ! current_user_can( 'manage_options' ) ) {
302 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'profile-builder' ) ) );
303 }
304
305 $posts = $this->get_forum_posts();
306
307 if ( is_wp_error( $posts ) ) {
308 wp_send_json_error( array( 'message' => $posts->get_error_message() ) );
309 }
310
311 wp_send_json_success( array( 'posts' => $posts ) );
312 }
313
314 /**
315 * AJAX handler to mark forum posts as read
316 */
317 public function ajax_mark_forum_posts_read() {
318 check_ajax_referer( 'wppb_support_chat', 'nonce' );
319
320 if ( ! current_user_can( 'manage_options' ) ) {
321 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'profile-builder' ) ) );
322 }
323
324 update_user_meta( get_current_user_id(), self::LAST_VIEWED_META_KEY, time() );
325
326 wp_send_json_success();
327 }
328
329 /**
330 * Get forum posts from RSS feed
331 *
332 * @return array|WP_Error
333 */
334 private function get_forum_posts() {
335 // Try to get from cache
336 $cached = get_transient( self::CACHE_KEY );
337 if ( false !== $cached ) {
338 return $cached;
339 }
340
341 // Fetch RSS feed
342 $response = wp_remote_get( self::FEED_URL, array(
343 'timeout' => 4,
344 'sslverify' => true,
345 ) );
346
347 if ( is_wp_error( $response ) ) {
348 set_transient( self::CACHE_KEY, array(), self::CACHE_DURATION );
349 return $response;
350 }
351
352 $body = wp_remote_retrieve_body( $response );
353 if ( empty( $body ) ) {
354 set_transient( self::CACHE_KEY, array(), self::CACHE_DURATION );
355 return new WP_Error( 'empty_feed', __( 'Empty feed response', 'profile-builder' ) );
356 }
357
358 // Parse XML
359 libxml_use_internal_errors( true );
360 $xml = simplexml_load_string( $body );
361 if ( false === $xml ) {
362 set_transient( self::CACHE_KEY, array(), self::CACHE_DURATION );
363 return new WP_Error( 'parse_error', __( 'Unable to parse feed', 'profile-builder' ) );
364 }
365
366 $posts = array();
367 $count = 0;
368
369 if ( isset( $xml->channel->item ) ) {
370 foreach ( $xml->channel->item as $item ) {
371 if ( $count >= self::POSTS_COUNT ) {
372 break;
373 }
374
375 // Get dc:creator namespace
376 $dc = $item->children( 'http://purl.org/dc/elements/1.1/' );
377
378 // Clean up title - strip HTML tags and decode entities
379 $title = (string) $item->title;
380 $title = strip_tags( $title );
381 $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' );
382 $title = trim( $title );
383
384 $posts[] = array(
385 'title' => $title,
386 'link' => (string) $item->link,
387 'date' => $this->format_date( (string) $item->pubDate ),
388 'timestamp' => (int) strtotime( (string) $item->pubDate ),
389 'author' => isset( $dc->creator ) ? (string) $dc->creator : '',
390 );
391
392 $count++;
393 }
394 }
395
396 // Cache the results
397 set_transient( self::CACHE_KEY, $posts, self::CACHE_DURATION );
398
399 return $posts;
400 }
401
402 /**
403 * Format date for display
404 *
405 * @param string $date_string
406 * @return string
407 */
408 private function format_date( $date_string ) {
409 $timestamp = strtotime( $date_string );
410 if ( ! $timestamp ) {
411 return '';
412 }
413
414 $now = time();
415 $diff = $now - $timestamp;
416
417 // Less than a day ago
418 if ( $diff < DAY_IN_SECONDS ) {
419 $hours = floor( $diff / HOUR_IN_SECONDS );
420 if ( $hours < 1 ) {
421 return __( 'Just now', 'profile-builder' );
422 }
423 /* translators: %d: number of hours */
424 return sprintf( _n( '%d hour ago', '%d hours ago', $hours, 'profile-builder' ), $hours );
425 }
426
427 // Less than a week ago
428 if ( $diff < WEEK_IN_SECONDS ) {
429 $days = floor( $diff / DAY_IN_SECONDS );
430 /* translators: %d: number of days */
431 return sprintf( _n( '%d day ago', '%d days ago', $days, 'profile-builder' ), $days );
432 }
433
434 // More than a week, show date
435 return date_i18n( get_option( 'date_format' ), $timestamp );
436 }
437 }
438
439 // Initialize
440 add_action( 'admin_init', array( 'WPPB_Support_Chat', 'get_instance' ) );
441