PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 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 All 37 releases
vikbooking / admin / helpers / widgets / guest_messages.php

guest_messages.php in VikBooking Hotel Booking Engine & PMS 1.8.7, at admin/helpers/widgets/guest_messages.php

2,055 lines 74.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2025 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Class handler for admin widget "guest messages".
15 *
16 * @since 1.16.0 (J) - 1.6.0 (WP)
17 */
18 class VikBookingAdminWidgetGuestMessages extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget.
22 *
23 * @var int
24 */
25 protected static $instance_counter = -1;
26
27 /**
28 * Number of messages per page. Should be an even number.
29 *
30 * @var int
31 */
32 protected $messages_per_page = 6;
33
34 /**
35 * Today Y-m-d string
36 *
37 * @var string
38 */
39 protected $today_ymd = null;
40
41 /**
42 * The path to the VCM lib to see if it's available.
43 *
44 * @var string
45 */
46 protected $vcm_lib_path = '';
47
48 /**
49 * Tells whether VCM is installed and updated.
50 *
51 * @var bool
52 */
53 protected $vcm_exists = true;
54
55 /**
56 * The distance threshold in pixels between the current scroll
57 * position and the end of the list for triggering the loading
58 * of a next page within an infinite scroll mechanism.
59 *
60 * @var int
61 *
62 * @since 1.17.6 (J) - 1.7.6 (WP)
63 */
64 protected $px_distance_threshold = 140;
65
66 /**
67 * Number of minimum messages per page when using the inbox-style (modal only).
68 *
69 * @var int
70 *
71 * @since 1.17.6 (J) - 1.7.6 (WP)
72 */
73 protected $inbox_messages_per_page = 12;
74
75 /**
76 * Class constructor will define the widget name and identifier.
77 */
78 public function __construct()
79 {
80 // call parent constructor
81 parent::__construct();
82
83 $this->widgetName = JText::translate('VBO_W_GUESTMESSAGES_TITLE');
84 $this->widgetDescr = JText::translate('VBO_W_GUESTMESSAGES_DESCR');
85 $this->widgetId = basename(__FILE__, '.php');
86
87 // define widget and icon and style name
88 $this->widgetIcon = '<i class="' . VikBookingIcons::i('comment-dots') . '"></i>';
89 $this->widgetStyleName = 'light-orange';
90
91 // load widget's settings
92 $this->widgetSettings = $this->loadSettings();
93 if (!is_object($this->widgetSettings)) {
94 $this->widgetSettings = new stdClass;
95 }
96
97 // today Y-m-d date
98 $this->today_ymd = date('Y-m-d');
99
100 // the path to the VCM library
101 $this->vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
102
103 // whether VCM is available
104 if (is_file($this->vcm_lib_path)) {
105 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getLatestFromGuests')) {
106 // VCM is outdated
107 $this->vcm_exists = false;
108 }
109
110 // attempt to require the chat handler
111 try {
112 VikBooking::getVcmChatInstance($oid = 0, $channel = null);
113 } catch (Exception $e) {
114 // do nothing
115 }
116
117 // make sure VCM is up to date for this widget
118 if (!class_exists('VCMChatHandler') || !method_exists('VCMChatHandler', 'loadChatAssets')) {
119 // VCM is outdated (>= 1.8.11 required)
120 $this->vcm_exists = false;
121 }
122 } else {
123 $this->vcm_exists = false;
124 }
125
126 // avoid queries on certain pages, as VCM may not have been activated yet
127 if (VBOPlatformDetection::isWordPress() && $this->vcm_exists) {
128 global $pagenow;
129 if (isset($pagenow) && in_array($pagenow, ['update.php', 'plugins.php', 'plugin-install.php'])) {
130 $this->vcm_exists = false;
131 }
132 }
133 }
134
135 /**
136 * Preload the necessary CSS/JS assets from VCM.
137 *
138 * @return void
139 */
140 public function preload()
141 {
142 if ($this->vcm_exists) {
143 // load chat assets from VCM
144 VCMChatHandler::loadChatAssets();
145
146 // datepicker calendar
147 $this->vbo_app->loadDatePicker();
148
149 // additional language defs
150 JText::script('VBO_NO_REPLY_NEEDED');
151 JText::script('VBO_WANT_PROCEED');
152 JText::script('VBOSIGNATURECLEAR');
153 JText::script('VBODASHSEARCHKEYS');
154 }
155 }
156
157 /**
158 * @inheritDoc
159 *
160 * @since 1.17.6 (J) - 1.7.6 (WP)
161 */
162 public function getWidgetDetails()
163 {
164 // get common widget details from parent abstract class
165 $details = parent::getWidgetDetails();
166
167 // append the modal rendering information
168 $details['modal'] = [
169 'add_class' => 'vbo-modal-large',
170 ];
171
172 return $details;
173 }
174
175 /**
176 * Custom method for this widget only to load the latest guest messages.
177 * The method is called by the admin controller through an AJAX request.
178 * The visibility should be public, it should not exit the process, and
179 * an associative array is returned thanks to the request value "return":1.
180 *
181 * It's the actual rendering of the widget which also allows navigation.
182 */
183 public function loadMessages()
184 {
185 $input = JFactory::getApplication()->input;
186
187 $bid_convo = $input->getString('bid_convo', '');
188 $filters = $input->get('filters', [], 'array');
189 $offset = $input->getInt('offset', 0);
190 $length = $input->getInt('length', $this->messages_per_page);
191 $wrapper = $input->getString('wrapper', '');
192
193 if (!$this->vcm_exists) {
194 VBOHttpDocument::getInstance()->close(500, 'Vik Channel Manager is either not available or outdated');
195 }
196
197 // check messages custom limit per page
198 if ($length > 0 && $length != $this->messages_per_page) {
199 // update widget settings
200 $this->widgetSettings->limpage = $length;
201 $this->updateSettings(json_encode($this->widgetSettings));
202 }
203
204 // build search filters
205 $search_filters = [
206 'guest_name' => $filters['guest_name'] ?? '',
207 'message' => $filters['message'] ?? '',
208 'sender' => $filters['sender'] ?? '',
209 'fromdt' => $filters['fromdt'] ?? '',
210 'todt' => $filters['todt'] ?? '',
211 'unread' => (int) ($filters['unread'] ?? 0),
212 'ai_sort' => (int) ($filters['ai_sort'] ?? 0),
213 ];
214
215 // filter out empty search filter values
216 $search_filters = array_filter($search_filters);
217
218 if (!empty($search_filters['fromdt'])) {
219 // convert the date string from local format to military
220 $search_filters['fromdt'] = date('Y-m-d H:i:s', VikBooking::getDateTimestamp($search_filters['fromdt'], 0, 0, 0));
221 // convert date from local timezone to UTC
222 $search_filters['fromdt'] = JFactory::getDate($search_filters['fromdt'], date_default_timezone_get())->format('Y-m-d H:i:s');
223 }
224
225 if (!empty($search_filters['todt'])) {
226 // convert the date string from local format to military
227 $search_filters['todt'] = date('Y-m-d H:i:s', VikBooking::getDateTimestamp($search_filters['todt'], 23, 59, 59));
228 // convert date from local timezone to UTC
229 $search_filters['todt'] = JFactory::getDate($search_filters['todt'], date_default_timezone_get())->format('Y-m-d H:i:s');
230 }
231
232 // initiate the chat messaging object
233 $chat_messaging = class_exists('VCMChatMessaging') ? VCMChatMessaging::getInstance() : null;
234
235 // last error description
236 $latest_error = '';
237
238 // load latest messages
239 $latest_messages = [];
240
241 try {
242 /**
243 * Search filters require an updated VCM version.
244 *
245 * @since 1.16.9 (J) - 1.6.9 (WP)
246 * @requires VCM >= 1.8.27 (1.9.5 "unread", "ai_sort")
247 */
248 if ($search_filters && $chat_messaging && method_exists($chat_messaging, 'searchMessages')) {
249 // search for specific messages with the specified search filters
250 $latest_messages = $chat_messaging->searchMessages($search_filters, $offset, $length);
251 } else {
252 // regular loading of the latest guest messages with no search filters
253 $latest_messages = VikChannelManager::getLatestFromGuests(['guest_messages'], $offset, $length);
254 }
255 } catch (Exception $e) {
256 // do nothing, but populate the error description
257 $latest_error = $e->getMessage();
258 }
259
260 // the multitask data and notifications can request a specific conversation to be opened
261 $bubble_convo = null;
262 if ($bid_convo) {
263 // make sure the requested booking ID was fetched from the most recent guest messages
264 foreach ($latest_messages as $gmessage) {
265 if ($bid_convo == $gmessage->idorder) {
266 // specific conversation to bubble found
267 $bubble_convo = $bid_convo;
268 break;
269 }
270 if ($bid_convo == $gmessage->idorderota && strcasecmp((string)$gmessage->channel, 'vikbooking')) {
271 // specific OTA conversation to bubble found
272 // make sure to use the internal booking ID rather than the OTA booking ID
273 $bid_convo = $gmessage->idorder ?: $bid_convo;
274 // request conversation to be popped up
275 $bubble_convo = $bid_convo;
276 break;
277 }
278 }
279 if (!$bubble_convo && $chat_messaging) {
280 // updated VCM versions will allow us to fetch one conversation by booking ID
281 $booking_messages = $chat_messaging->loadBookingGuestThreads($bid_convo);
282 if ($booking_messages) {
283 // append the requested conversation so that it will bubble
284 $latest_messages = array_merge($latest_messages, $booking_messages);
285 // turn flag on
286 $bubble_convo = $bid_convo;
287 }
288 }
289 }
290
291 // current year Y and timestamp
292 $current_y = date('Y');
293 $current_ts = time();
294
295 // start output buffering
296 ob_start();
297
298 if ($latest_error) {
299 ?>
300 <p class="err"><?php echo $latest_error; ?></p>
301 <?php
302 }
303
304 if (!$latest_messages) {
305 ?>
306 <p class="info" data-no-messages="1" style="<?php echo $offset > 0 ? 'text-align: center;' : ''; ?>"><?php echo $offset > 0 ? JText::translate('VBO_NO_MORE_MESSAGES') : JText::translate('VBO_NO_RECORDS_FOUND'); ?></p>
307 <?php
308 }
309
310 // count total messages
311 $tot_messages = count($latest_messages);
312
313 foreach ($latest_messages as $ind => $gmessage) {
314 $gmessage_content = $gmessage->content;
315 if (empty($gmessage_content)) {
316 $gmessage_content = '.....';
317 } elseif (strlen($gmessage_content) > 90) {
318 if (function_exists('mb_substr')) {
319 $gmessage_content = mb_substr($gmessage_content, 0, 90, 'UTF-8');
320 } else {
321 $gmessage_content = substr($gmessage_content, 0, 90);
322 }
323 $gmessage_content .= '...';
324 }
325
326 // build extra classes for main element
327 $wrap_classes = [];
328 if ($ind === 0) {
329 $wrap_classes[] = 'vbo-w-guestmessages-message-first';
330 } elseif ($ind == ($tot_messages - 1)) {
331 $wrap_classes[] = 'vbo-w-guestmessages-message-last';
332 }
333 if (empty($gmessage->read_dt) && !strcasecmp($gmessage->sender_type, 'guest')) {
334 $wrap_classes[] = 'vbo-w-guestmessages-message-new';
335 }
336
337 ?>
338 <div
339 class="vbo-dashboard-guest-activity vbo-w-guestmessages-message<?php echo $wrap_classes ? ' ' . implode(' ', $wrap_classes) : ''; ?>"
340 data-idorder="<?php echo $gmessage->idorder; ?>"
341 data-idthread="<?php echo !empty($gmessage->id_thread) ? $gmessage->id_thread : ''; ?>"
342 data-idmessage="<?php echo !empty($gmessage->id_message) ? $gmessage->id_message : ''; ?>"
343 data-noreply-needed="<?php echo $gmessage->no_reply_needed ?: 0; ?>"
344 onclick="vboWidgetGuestMessagesOpenChat('<?php echo $gmessage->idorder; ?>');"
345 >
346 <div class="vbo-dashboard-guest-activity-avatar">
347 <?php
348 if (!empty($gmessage->guest_avatar)) {
349 // highest priority goes to the profile picture, not always available
350 ?>
351 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $gmessage->guest_avatar; ?>" />
352 <?php
353 } elseif (!empty($gmessage->pic)) {
354 // customer profile picture is not the same as the photo avatar
355 ?>
356 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo strpos($gmessage->pic, 'http') === 0 ? $gmessage->pic : VBO_SITE_URI . 'resources/uploads/' . $gmessage->pic; ?>" />
357 <?php
358 } elseif (!empty($gmessage->channel_logo)) {
359 // channel logo goes as second option
360 ?>
361 <img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $gmessage->channel_logo; ?>" />
362 <?php
363 } else {
364 // we use an icon as fallback
365 VikBookingIcons::e('user', 'vbo-dashboard-guest-activity-avatar-icon');
366 }
367
368 // check for AI priority enum
369 if (!empty($gmessage->ai_priority)) {
370 // display AI-calculated priority icon and apposite class (expected "high", "medium" or "low")
371 $priority_icon = 'flag';
372 $priority_class = preg_replace("/[^a-z]/", '', strtolower((string) $gmessage->ai_priority));
373 $priority_value = ucwords((string) $gmessage->ai_priority);
374 if (!strcasecmp(trim((string) $gmessage->ai_priority), 'high')) {
375 $priority_value = JText::translate('VBO_PRIORITY_HIGH');
376 } elseif (!strcasecmp(trim((string) $gmessage->ai_priority), 'medium')) {
377 $priority_value = JText::translate('VBO_PRIORITY_MEDIUM');
378 } elseif (!strcasecmp(trim((string) $gmessage->ai_priority), 'low')) {
379 $priority_value = JText::translate('VBO_PRIORITY_LOW');
380 }
381 ?>
382 <div class="vbo-w-guestmessages-message-aipriority">
383 <span class="vbo-w-guestmessages-message-aipriority-icn <?php echo $priority_class; ?> vbo-tooltip vbo-tooltip-right" data-tooltiptext="<?php echo JHtml::fetch('esc_attr', $priority_value); ?>"><?php VikBookingIcons::e($priority_icon, $priority_class); ?></span>
384 </div>
385 <?php
386 }
387 ?>
388 </div>
389 <div class="vbo-dashboard-guest-activity-content">
390 <div class="vbo-dashboard-guest-activity-content-head">
391 <div class="vbo-dashboard-guest-activity-content-info-details">
392 <h4 class="vbo-w-guestmessages-message-gtitle"><span><?php
393 if (!$gmessage->first_name && !$gmessage->last_name) {
394 echo JText::translate('VBO_GUEST');
395 } else {
396 echo $gmessage->first_name . (!empty($gmessage->last_name) ? ' ' . $gmessage->last_name : '');
397 }
398 ?></span><?php
399 if (empty($gmessage->read_dt) && !strcasecmp($gmessage->sender_type, 'guest')) {
400 // print also an icon to inform that the message was not read
401 echo ' ';
402 VikBookingIcons::e('envelope', 'message-new');
403 } elseif (($gmessage->replied ?? 1) == 0 && !strcasecmp($gmessage->sender_type, 'guest')) {
404 /**
405 * Display a label to show that the message was not replied.
406 *
407 * @since 1.16.9 (J) - 1.6.9 (WP)
408 *
409 * @requires VCM >= 1.8.27
410 */
411 echo ' <span class="label label-small message-unreplied">';
412 VikBookingIcons::e('comments', 'message-reply');
413 echo ' ' . JText::translate('VBO_REPLY') . '</span>';
414 }
415
416 /**
417 * Display the AI message category, if available.
418 *
419 * @since 1.17.3 (J) - 1.7.3 (WP)
420 *
421 * @requires VCM >= 1.9.5
422 */
423 if (!empty($gmessage->ai_category)) {
424 echo ' <span class="label label-small message-ai-category">';
425 VikBookingIcons::e('tag');
426 echo ' ' . $gmessage->ai_category . '</span>';
427 }
428 ?></h4>
429 <div class="vbo-dashboard-guest-activity-content-info-icon">
430 <?php
431 if (!empty($gmessage->b_status)) {
432 switch ($gmessage->b_status) {
433 case 'standby':
434 $badge_class = 'badge-warning';
435 $badge_text = JText::translate('VBSTANDBY');
436 break;
437 case 'cancelled':
438 $badge_class = 'badge-danger';
439 $badge_text = JText::translate('VBCANCELLED');
440 break;
441 default:
442 $badge_class = 'badge-success';
443 $badge_text = JText::translate('VBCONFIRMED');
444 if (!empty($gmessage->b_checkout) && $gmessage->b_checkout < $current_ts) {
445 $badge_text = JText::translate('VBOCHECKEDSTATUSOUT');
446 }
447 break;
448 }
449 ?>
450 <span class="badge <?php echo $badge_class; ?>"><?php echo $badge_text; ?></span>
451 <?php
452 }
453 if (!empty($gmessage->b_checkin)) {
454 $stay_info_in = getdate($gmessage->b_checkin);
455 $stay_info_out = getdate($gmessage->b_checkout);
456 $str_checkin = date('d', $gmessage->b_checkin);
457 $str_checkin .= $stay_info_in['mon'] != $stay_info_out['mon'] ? ' ' . VikBooking::sayMonth($stay_info_in['mon'], $short = true) : '';
458 $str_checkout = date('d', $gmessage->b_checkout) . ' ' . VikBooking::sayMonth($stay_info_out['mon'], $short = true);
459 if ($stay_info_in['year'] != $stay_info_out['year'] || $stay_info_in['year'] != $current_y || $stay_info_out['year'] != $current_y) {
460 $str_checkout .= ' ' . $stay_info_in['year'];
461 }
462 ?>
463 <span class="vbo-w-guestmessages-message-staydates">
464 <span class="vbo-w-guestmessages-message-staydates-in"><?php echo $str_checkin; ?></span>
465 <span class="vbo-w-guestmessages-message-staydates-sep">-</span>
466 <span class="vbo-w-guestmessages-message-staydates-out"><?php echo $str_checkout; ?></span>
467 </span>
468 <?php
469 }
470 ?>
471 </div>
472 </div>
473 <div class="vbo-dashboard-guest-activity-content-info-date">
474 <span><?php echo JHtml::fetch('date', $gmessage->last_updated, 'H:i'); ?></span>
475 <?php
476 if (JHtml::fetch('date', $gmessage->last_updated, 'Y-m-d') != $this->today_ymd) {
477 // format and print the date
478 ?>
479 <span><?php echo JHtml::fetch('date', $gmessage->last_updated, str_replace('/', $this->datesep, $this->df)); ?></span>
480 <?php
481 } else {
482 // print "today"
483 ?>
484 <span><?php echo JText::translate('VBTODAY'); ?></span>
485 <?php
486 }
487 ?>
488 </div>
489 </div>
490 <div class="vbo-dashboard-guest-activity-content-info-msg">
491 <p><?php echo $gmessage_content; ?></p>
492 </div>
493 </div>
494 </div>
495 <?php
496 }
497
498 // append navigation
499 ?>
500 <div class="vbo-guestactivitywidget-commands">
501 <div class="vbo-guestactivitywidget-commands-main">
502 <?php
503 if ($offset > 0) {
504 // show backward navigation button
505 ?>
506 <div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-prev">
507 <span class="vbo-guestactivitywidget-prev" onclick="vboWidgetGuestMessagesNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
508 </div>
509 <?php
510 }
511 if ($latest_messages) {
512 // count current page number
513 $page_number = 1;
514 if ($offset > 0) {
515 $page_number = floor($offset / $length) + 1;
516 $page_number = $page_number > 0 ? $page_number : 1;
517 }
518 ?>
519 <div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-page">
520 <span class="vbo-guestactivitywidget-page"><?php echo JText::sprintf('VBO_PAGE_NUMBER', $page_number); ?></span>
521 </div>
522 <div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-next">
523 <span class="vbo-guestactivitywidget-next" onclick="vboWidgetGuestMessagesNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
524 </div>
525 <?php
526 }
527 ?>
528 </div>
529 </div>
530
531 <script type="text/javascript">
532 setTimeout(() => {
533 <?php
534 // check if there were actually no filters applied
535 if (!$search_filters || (count($search_filters) === 1 && ($search_filters['ai_sort'] ?? 0))) {
536 ?>
537 jQuery('#<?php echo $wrapper; ?>-filters').hide();
538 <?php
539 }
540
541 // check if results were sorted through AI
542 if ($search_filters['ai_sort'] ?? 0) {
543 ?>
544 jQuery('#<?php echo $wrapper; ?>-aipowered').fadeIn();
545 <?php
546 } else {
547 ?>
548 jQuery('#<?php echo $wrapper; ?>-aipowered').hide();
549 <?php
550 }
551 ?>
552 }, 200);
553 </script>
554 <?php
555
556 // check if we should bubble a specific conversation
557 if ($bubble_convo) {
558 ?>
559 <script type="text/javascript">
560 setTimeout(() => {
561 vboWidgetGuestMessagesOpenChat('<?php echo $bubble_convo; ?>');
562 }, 400);
563 </script>
564 <?php
565 }
566
567 // append the total number of messages displayed, the current offset and the latest message datetime
568 $latest_datetime = !$search_filters && $tot_messages > 0 && $offset === 0 ? $latest_messages[0]->last_updated : null;
569
570 // get the HTML buffer
571 $html_content = ob_get_contents();
572 ob_end_clean();
573
574 // return an associative array of values
575 return [
576 'html' => $html_content,
577 'tot_messages' => $tot_messages,
578 'offset' => ($offset + $length),
579 'latest_dt' => $latest_datetime,
580 ];
581 }
582
583 /**
584 * Custom method for this widget only to watch the latest guest messages.
585 * The method is called by the admin controller through an AJAX request.
586 * The visibility should be public, it should not exit the process, and
587 * any content sent to output will be returned to the AJAX response.
588 *
589 * Outputs the new number of messages found from the latest datetime.
590 */
591 public function watchMessages()
592 {
593 $latest_dt = VikRequest::getString('latest_dt', '', 'request');
594 if (empty($latest_dt)) {
595 echo '0';
596 return;
597 }
598
599 if (!$this->vcm_exists) {
600 VBOHttpDocument::getInstance()->close(500, 'Vik Channel Manager is either not available or outdated');
601 }
602
603 // load the latest guest message (one is sufficient)
604 $latest_messages = [];
605 try {
606 $latest_messages = VikChannelManager::getLatestFromGuests(['guest_messages'], 0, 1);
607 } catch (Exception $e) {
608 // do nothing
609 }
610
611 if (!$latest_messages || $latest_messages[0]->last_updated == $latest_dt) {
612 // no newest messages found
613 echo '0';
614 return;
615 }
616
617 // print 1 to indicate that new messages should be reloaded
618 echo '1';
619 }
620
621 /**
622 * Custom method for this widget only to render the chat of a booking.
623 * The method is called by the admin controller through an AJAX request.
624 * The visibility should be public, it should not exit the process, and
625 * no values should be returned, as the response must be sent to output
626 * in case the JS/CSS assets will be echoed within the response.
627 *
628 * Returns the necessary HTML code to render the chat.
629 */
630 public function renderChat()
631 {
632 $bid = VikRequest::getInt('bid', 0, 'request');
633
634 $booking = VikBooking::getBookingInfoFromID($bid);
635
636 if (!$booking) {
637 VBOHttpDocument::getInstance()->close(404, 'Could not find booking');
638 }
639
640 // initialize chat instance by getting the proper channel name
641 if (empty($booking['channel'])) {
642 // front-end reservation chat handler
643 $chat_channel = 'vikbooking';
644 } else {
645 $channelparts = explode('_', $booking['channel']);
646 // check if this is a meta search channel
647 $is_meta_search = false;
648 if (preg_match("/(customer).*[0-9]$/", $channelparts[0]) || !strcasecmp($channelparts[0], 'googlehotel') || !strcasecmp($channelparts[0], 'googlevr') || !strcasecmp($channelparts[0], 'trivago')) {
649 $is_meta_search = empty($booking['idorderota']);
650 }
651 if ($is_meta_search) {
652 // customer of type sales channel should use front-end reservation chat handler
653 $chat_channel = 'vikbooking';
654 } else {
655 // let the getInstance method validate the channel chat handler
656 $chat_channel = $booking['channel'];
657 }
658 }
659 $messaging = VikBooking::getVcmChatInstance($booking['id'], $chat_channel);
660
661 if (is_null($messaging)) {
662 VBOHttpDocument::getInstance()->close(500, 'Could not render chat');
663 }
664
665 // send content to output
666 echo $messaging->renderChat([
667 'hideThreads' => 1,
668 ], $load_assets = false);
669 }
670
671 /**
672 * Custom method for this widget only to update the thread of a booking.
673 * The method is called by the admin controller through an AJAX request.
674 * The visibility should be public, it should not exit the process, and
675 * no values should be returned, as the response must be sent to output.
676 *
677 * Returns a successful string or throws an error.
678 */
679 public function setNoReplyNeededThread()
680 {
681 $bid = VikRequest::getInt('bid', 0, 'request');
682 $id_thread = VikRequest::getInt('id_thread', 0, 'request');
683 $status = VikRequest::getInt('status', 0, 'request');
684
685 $booking = VikBooking::getBookingInfoFromID($bid);
686
687 if (!$booking || empty($id_thread)) {
688 VBOHttpDocument::getInstance()->close(404, 'Could not find booking thread');
689 }
690
691 // build thread object for update
692 $thread = new stdClass;
693 $thread->id = $id_thread;
694 $thread->idorder = $bid;
695 $thread->no_reply_needed = !$status ? 1 : 0;
696
697 $dbo = JFactory::getDbo();
698
699 if (!$dbo->updateObject('#__vikchannelmanager_threads', $thread, ['id', 'idorder'])) {
700 VBOHttpDocument::getInstance()->close(500, 'Could not update thread');
701 }
702
703 echo '1';
704 }
705
706 /**
707 * Custom method for this widget only to load the listing details for the booking.
708 * The method is called by the admin controller through an AJAX request.
709 * The visibility should be public, it should not exit the process, and
710 * an associative array is returned thanks to the request value "return":1.
711 *
712 * Needed to provide additional information to the host about the booked listing(s).
713 */
714 public function loadListingDetails()
715 {
716 $bid = VikRequest::getInt('bid', 0, 'request');
717
718 $booking_rooms = VikBooking::loadOrdersRoomsData($bid);
719
720 if (!$this->vcm_exists || !$booking_rooms) {
721 VBOHttpDocument::getInstance()->close(500, 'Could not obtain the listing information');
722 }
723
724 return [
725 'listings' => array_column($booking_rooms, 'room_name'),
726 ];
727 }
728
729 public function render(?VBOMultitaskData $data = null)
730 {
731 // increase widget's instance counter
732 static::$instance_counter++;
733
734 // check whether the widget is being rendered via AJAX when adding it through the customizer
735 $is_ajax = $this->isAjaxRendering();
736
737 // generate a unique ID for the guest messages wrapper instance
738 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
739 $wrapper_id = 'vbo-widget-guest-messages-' . $wrapper_instance;
740
741 // this widget will work only if VCM is available and updated, and if permissions are met
742 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
743 if (!$this->vcm_exists || !$vbo_auth_bookings) {
744 return;
745 }
746
747 // multitask data event identifier for clearing intervals
748 $js_intvals_id = '';
749 $wrap_extra_class = '';
750 $bid_convo = 0;
751 $unread_filter = false;
752 $sort_by_ai = false;
753 $contains_filter = '';
754 if ($data && $data->isModalRendering()) {
755 // access Multitask data
756 $js_intvals_id = $data->getModalJsIdentifier();
757
758 // check if a specific conversation should be opened
759 if ($data->get('id_message', 0)) {
760 $bid_convo = $data->getBookingId();
761 } else {
762 $bid_convo = $this->options()->fetchBookingId();
763 }
764
765 // set an extra class
766 $wrap_extra_class = ' vbo-w-guestmessages-wrapmodal';
767
768 // check for unread messages filter
769 $unread_filter = (bool) $this->options()->get('unread', '');
770
771 // check for AI sort filter
772 $sort_by_ai = (bool) $this->options()->get('ai_sort', '');
773
774 // check if a specific filter was set for searching guest messages
775 $contains_filter = (string) $this->options()->get('message_contains', '');
776
777 // check custom limit per page only when in modal rendering
778 if (($this->widgetSettings->limpage ?? 0) > 0) {
779 // set custom limit from widget settings
780 $this->messages_per_page = (int) $this->widgetSettings->limpage;
781 }
782
783 // when modal rendering, we expect to adopt the inbox-style by default unless in small screens
784 if ($this->messages_per_page < $this->inbox_messages_per_page) {
785 // in order to facilitate the infinite scroll, we use the minimum number of messages per page
786 $this->messages_per_page = $this->inbox_messages_per_page;
787 }
788
789 // check for limit filter injected
790 $limpage_filter = (int) $this->options()->get('limpage', 0);
791 if ($limpage_filter > 0) {
792 // set custom limit
793 $this->messages_per_page = $limpage_filter;
794 }
795 }
796
797 ?>
798 <div class="vbo-admin-widget-wrapper">
799 <div class="vbo-admin-widget-head">
800 <div class="vbo-admin-widget-head-inline">
801 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
802 <div class="vbo-admin-widget-head-commands">
803 <div class="vbo-reportwidget-commands">
804 <div class="vbo-reportwidget-commands-main">
805 <div class="vbo-reportwidget-command-dates">
806 <div id="<?php echo $wrapper_id; ?>-filters" class="vbo-reportwidget-period-name" style="<?php echo !$contains_filter && !$unread_filter ? 'display: none;' : ''; ?>"><?php VikBookingIcons::e('filter'); ?> <?php echo JText::translate('VBO_FILTERS_APPLIED'); ?></div>
807 </div>
808 <div id="<?php echo $wrapper_id; ?>-aipowered" class="vbo-admin-widget-head-ai-powered" style="display: none;">
809 <span class="label label-info"><?php echo JText::translate('VBO_AI_LABEL_DEF'); ?></span>
810 </div>
811 </div>
812 <div class="vbo-reportwidget-command-dots">
813 <span class="vbo-widget-command-togglefilters vbo-widget-guest-messages-togglefilters" onclick="vboWidgetGuestMessagesOpenSettings('<?php echo $wrapper_id; ?>');">
814 <?php VikBookingIcons::e('ellipsis-v'); ?>
815 </span>
816 </div>
817 </div>
818 </div>
819 </div>
820 </div>
821 <div id="<?php echo $wrapper_id; ?>" class="vbo-dashboard-guests-latest<?php echo $wrap_extra_class; ?>" data-offset="0" data-length="<?php echo $this->messages_per_page; ?>" data-eventsid="<?php echo $js_intvals_id; ?>" data-latestdt="">
822 <div class="vbo-dashboard-guest-messages-inner">
823 <div class="vbo-w-guestmessages-list-container">
824 <div class="vbo-dashboard-guest-messages-list">
825 <?php
826 for ($i = 0; $i < $this->messages_per_page; $i++) {
827 ?>
828 <div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">
829 <div class="vbo-dashboard-guest-activity-avatar">
830 <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>
831 </div>
832 <div class="vbo-dashboard-guest-activity-content">
833 <div class="vbo-dashboard-guest-activity-content-head">
834 <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>
835 </div>
836 <div class="vbo-dashboard-guest-activity-content-subhead">
837 <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>
838 </div>
839 <div class="vbo-dashboard-guest-activity-content-info-msg">
840 <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>
841 </div>
842 </div>
843 </div>
844 <?php
845 }
846 ?>
847 </div>
848 </div>
849 <div class="vbo-w-guestmessages-inboxstyle-chat"></div>
850 </div>
851 <div class="vbo-widget-guest-messages-filters-hidden" style="display: none;">
852 <div class="vbo-widget-guest-messages-filters-wrap">
853 <div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">
854 <div class="vbo-params-wrap">
855 <div class="vbo-params-container">
856 <div class="vbo-params-block">
857
858 <div class="vbo-param-container">
859 <div class="vbo-param-label"><?php echo JText::translate('VBCUSTOMERNOMINATIVE'); ?></div>
860 <div class="vbo-param-setting">
861 <input type="text" class="vbo-widget-guest-messages-guestname" value="" autocomplete="off" />
862 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_FIRST_NAME_ACCURATE_HELP'); ?></span>
863 </div>
864 </div>
865
866 <div class="vbo-param-container">
867 <div class="vbo-param-label"><?php echo JText::translate('VBSENDEMAILCUSTCONT'); ?></div>
868 <div class="vbo-param-setting">
869 <input type="text" class="vbo-widget-guest-messages-messcontains" value="<?php echo JHtml::fetch('esc_attr', $contains_filter); ?>" autocomplete="off" />
870 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_MESSAGE_CONTAINS_HELP'); ?></span>
871 </div>
872 </div>
873
874 <div class="vbo-param-container vbo-toggle-small">
875 <div class="vbo-param-label"><?php echo JText::translate('VBO_UNREAD_MESSAGES'); ?></div>
876 <div class="vbo-param-setting">
877 <?php echo $this->vbo_app->printYesNoButtons('unread', JText::translate('VBYES'), JText::translate('VBNO'), (int) $unread_filter, 1, 0, '', ['blue']); ?>
878 </div>
879 </div>
880
881 <div class="vbo-param-container vbo-toggle-small">
882 <div class="vbo-param-label"><?php echo JText::translate('VBO_AI_LABEL_DEF') . ' ' . JText::translate('VBO_SORT_BY_PRIORITY'); ?></div>
883 <div class="vbo-param-setting">
884 <?php echo $this->vbo_app->printYesNoButtons('ai_sort', JText::translate('VBYES'), JText::translate('VBNO'), (int) $sort_by_ai, 1, 0, '', ['gold']); ?>
885 </div>
886 </div>
887
888 <div class="vbo-param-container vbo-toggle-small">
889 <div class="vbo-param-label"><?php echo JText::translate('VBO_SENDER'); ?></div>
890 <div class="vbo-param-setting">
891 <div class="vbo-widget-guest-messages-multistate">
892 <?php
893 echo $this->vbo_app->multiStateToggleSwitchField(
894 'sender' . $wrapper_instance,
895 '',
896 [
897 'guest',
898 'hotel',
899 ],
900 [
901 [
902 'value' => JText::translate('VBO_GUEST'),
903 ],
904 [
905 'value' => 'Hotel',
906 ],
907 ],
908 [
909 [
910 'label_class' => 'vik-multiswitch-text vik-multiswitch-radiobtn-guest',
911 'input' => [
912 'class' => 'vbo-widget-guest-messages-filter-sender',
913 ],
914 ],
915 [
916 'label_class' => 'vik-multiswitch-text vik-multiswitch-radiobtn-hotel',
917 'input' => [
918 'class' => 'vbo-widget-guest-messages-filter-sender',
919 ],
920 ],
921 ],
922 [
923 'class' => 'vik-multiswitch-noanimation',
924 ]
925 );
926 ?>
927 </div>
928 </div>
929 </div>
930
931 <div class="vbo-param-container">
932 <div class="vbo-param-label"><?php echo JText::translate('VBNEWRESTRICTIONDFROMRANGE'); ?></div>
933 <div class="vbo-param-setting">
934 <div class="vbo-field-calendar">
935 <div class="input-append">
936 <input type="text" class="vbo-widget-guest-messages-fromdt" value="" autocomplete="off" />
937 <button type="button" class="btn btn-secondary vbo-widget-guest-messages-fromdt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
938 </div>
939 </div>
940 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_FROM_DT_HELP'); ?></span>
941 </div>
942 </div>
943
944 <div class="vbo-param-container">
945 <div class="vbo-param-label"><?php echo JText::translate('VBNEWRESTRICTIONDTORANGE'); ?></div>
946 <div class="vbo-param-setting">
947 <div class="vbo-field-calendar">
948 <div class="input-append">
949 <input type="text" class="vbo-widget-guest-messages-todt" value="" autocomplete="off" />
950 <button type="button" class="btn btn-secondary vbo-widget-guest-messages-todt-trigger"><?php VikBookingIcons::e('calendar'); ?></button>
951 </div>
952 </div>
953 <span class="vbo-param-setting-comment"><?php echo JText::translate('VBO_TO_DT_HELP'); ?></span>
954 </div>
955 </div>
956
957 <div class="vbo-param-container">
958 <div class="vbo-param-label"><?php echo JText::translate('VBO_MESS_PER_PAGE'); ?></div>
959 <div class="vbo-param-setting">
960 <input type="number" class="vbo-widget-guest-messages-limpage" min="1" max="100" value="<?php echo $this->messages_per_page; ?>" />
961 </div>
962 </div>
963
964 </div>
965 </div>
966 </div>
967 </div>
968 </div>
969 </div>
970 </div>
971 </div>
972
973 <?php
974 if (static::$instance_counter === 0 || $is_ajax) {
975 // HTML helper tag for URL routing and some JS functions should be loaded once per widget instance
976 $admin_file_base = VBOPlatformDetection::isWordPress() ? 'admin.php' : 'index.php';
977 ?>
978
979 <script type="text/javascript">
980
981 /**
982 * Open the settings to search/filter the guest messages.
983 */
984 function vboWidgetGuestMessagesOpenSettings(wrapper) {
985 var widget_instance = jQuery('#' + wrapper);
986 if (!widget_instance.length) {
987 return false;
988 }
989
990 // determine the messages rendering method
991 var mess_rendering_meth = widget_instance.hasClass('vbo-w-guestmessages-inboxstyle') ? 'inbox' : 'paging';
992
993 // define unique modal event name to avoid conflicts
994 var eventsid = widget_instance.attr('data-eventsid') || (Math.floor(Math.random() * 100000));
995 var modal_dismiss_event = 'dismiss-modal-wguestmessages-search' + eventsid;
996
997 // the hidden container of the search input fields
998 var search_elements = widget_instance.find('.vbo-widget-guest-messages-filters-wrap');
999
1000 // build the button element to apply the search filters
1001 var apply_search_btn = document.createElement('button');
1002 apply_search_btn.setAttribute('type', 'button');
1003 apply_search_btn.classList.add('btn', 'btn-success');
1004 apply_search_btn.append(document.createTextNode(Joomla.JText._('VBODASHSEARCHKEYS')));
1005 apply_search_btn.addEventListener('click', () => {
1006 VBOCore.emitEvent(modal_dismiss_event, JSON.stringify({applyfilters: 1, wrapper: wrapper}));
1007 });
1008
1009 // build the button element to clear the search filters
1010 var clear_search_btn = document.createElement('button');
1011 clear_search_btn.setAttribute('type', 'button');
1012 clear_search_btn.classList.add('btn');
1013 clear_search_btn.append(document.createTextNode(Joomla.JText._('VBOSIGNATURECLEAR')));
1014 clear_search_btn.addEventListener('click', () => {
1015 VBOCore.emitEvent(modal_dismiss_event, JSON.stringify({clearfilters: 1, wrapper: wrapper}));
1016 });
1017
1018 var search_modal_body = VBOCore.displayModal({
1019 suffix: 'wguestmessages-search',
1020 extra_class: 'vbo-modal-rounded vbo-modal-dialog',
1021 title: '<?php echo JHtml::fetch('esc_attr', JText::translate('VBO_W_GUESTMESSAGES_TITLE')); ?> - ' + Joomla.JText._('VBODASHSEARCHKEYS'),
1022 body_prepend: true,
1023 draggable: true,
1024 footer_left: clear_search_btn,
1025 footer_right: apply_search_btn,
1026 dismiss_event: modal_dismiss_event,
1027 onDismiss: (e) => {
1028 // always move back the search input fields
1029 search_elements.appendTo(widget_instance.find('.vbo-widget-guest-messages-filters-hidden'));
1030
1031 if (!e || !e.detail) {
1032 // no event data received, maybe the modal was simply dismissed
1033 jQuery('#' + wrapper + '-filters').hide();
1034 return;
1035 }
1036
1037 // parse data received within the dismiss event
1038 try {
1039 let commands = JSON.parse(e.detail);
1040
1041 if (!commands['wrapper']) {
1042 return;
1043 }
1044
1045 // determine if we are resetting the widget
1046 let is_resetting = false;
1047
1048 if (commands['applyfilters']) {
1049 if (mess_rendering_meth == 'inbox') {
1050 // turn flag on
1051 is_resetting = true;
1052 // empty messages list
1053 widget_instance.find('.vbo-dashboard-guest-messages-list').html('');
1054 // destroy any previous chat instance
1055 widget_instance.find('.vbo-w-guestmessages-inboxstyle-chat').html('');
1056 if (typeof VCMChat !== 'undefined') {
1057 VCMChat.getInstance().destroy();
1058 }
1059 // destroy inifite scroll events
1060 vboWidgetGuestMessagesDestroyInfiniteScroll(wrapper);
1061 }
1062
1063 // display filters applied label
1064 jQuery('#' + wrapper + '-filters').show();
1065 // reset offset to 0
1066 widget_instance.attr('data-offset', 0);
1067 // show loading skeletons
1068 vboWidgetGuestMessagesSkeletons(commands['wrapper']);
1069 // reload guest messages for this widget's instance with filters set
1070 vboWidgetGuestMessagesLoad(commands['wrapper'], null, is_resetting);
1071 }
1072
1073 if (commands['clearfilters']) {
1074 if (mess_rendering_meth == 'inbox') {
1075 // turn flag on
1076 is_resetting = true;
1077 // empty messages list
1078 widget_instance.find('.vbo-dashboard-guest-messages-list').html('');
1079 // destroy any previous chat instance
1080 widget_instance.find('.vbo-w-guestmessages-inboxstyle-chat').html('');
1081 if (typeof VCMChat !== 'undefined') {
1082 VCMChat.getInstance().destroy();
1083 }
1084 // destroy inifite scroll events
1085 vboWidgetGuestMessagesDestroyInfiniteScroll(wrapper);
1086 }
1087
1088 // clear filters
1089 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="text"]').val('');
1090 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="number"]').val('');
1091 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="checkbox"]').prop('checked', false);
1092 widget_instance.find('.vbo-widget-guest-messages-filters-wrap').find('input[type="radio"]').prop('checked', false);
1093 // hide filters applied label
1094 jQuery('#' + wrapper + '-filters').hide();
1095 // reset offset to 0
1096 widget_instance.attr('data-offset', 0);
1097 // show loading skeletons
1098 vboWidgetGuestMessagesSkeletons(commands['wrapper']);
1099 // reload guest messages for this widget's instance with filters cleared
1100 vboWidgetGuestMessagesLoad(commands['wrapper'], null, is_resetting);
1101 }
1102 } catch(e) {
1103 // abort
1104 return;
1105 }
1106 },
1107 });
1108
1109 // move the search filter fields to the modal body
1110 search_elements.appendTo(search_modal_body);
1111 }
1112
1113 /**
1114 * Open the chat for the clicked booking guest message
1115 */
1116 function vboWidgetGuestMessagesOpenChat(id) {
1117 // clicked message
1118 var message_el = jQuery('.vbo-w-guestmessages-message[data-idorder="' + id + '"]').first();
1119
1120 if (message_el.hasClass('vbo-w-guestmessages-message-new')) {
1121 // get rid of the "new/unread" status
1122 message_el.removeClass('vbo-w-guestmessages-message-new');
1123 if (message_el.find('i.message-new').length) {
1124 message_el.find('i.message-new').remove();
1125 }
1126 }
1127
1128 // set active message class only on the clicked message
1129 jQuery('.vbo-w-guestmessages-message').removeClass('vbo-inbox-active-message');
1130 message_el.addClass('vbo-inbox-active-message');
1131
1132 // get widget's main block and chat container
1133 var message_block = message_el.closest('.vbo-dashboard-guests-latest');
1134 var chat_inline_container = message_block.find('.vbo-w-guestmessages-inboxstyle-chat');
1135
1136 // determine the chat rendering method (modal or inline)
1137 var chat_rendering_meth = message_block.hasClass('vbo-w-guestmessages-inboxstyle') ? 'inline' : 'modal';
1138
1139 // destroy any previous chat instance
1140 if (typeof VCMChat !== 'undefined') {
1141 VCMChat.getInstance().destroy();
1142 }
1143
1144 // always empty chat container
1145 chat_inline_container.html('');
1146
1147 // modal events unique id to avoid conflicts
1148 var eventsid = message_block.attr('data-eventsid') || (Math.floor(Math.random() * 100000));
1149
1150 // define unique modal event names to avoid conflicts
1151 var modal_dismiss_event = 'dismiss-modal-wguestmessages-chat' + eventsid;
1152 var modal_loading_event = 'loading-modal-wguestmessages-chat' + eventsid;
1153
1154 // check for multiple instances of this widget, maybe because of clicked notifications while another instance was displayed
1155 if (jQuery('.vbo-w-guestmessages-message[data-idorder="' + id + '"]').length > 1) {
1156 // multiple instances found
1157 if (message_block.attr('data-eventsid')) {
1158 // dismiss the previous modal and keep using the same event id to ensure a de-registration of the modal events
1159 VBOCore.emitEvent(modal_dismiss_event);
1160 } else {
1161 // fallback to using a random events id to avoid conflicts
1162 eventsid = Math.floor(Math.random() * 100000);
1163 }
1164 }
1165
1166 // build modal content
1167 var chat_head_title = jQuery('<span></span>');
1168 var chat_head_title_wrap = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-info');
1169 var chat_head_title_top = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-info-customer');
1170 var chat_head_title_bot = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-info-booking');
1171
1172 var chat_head_title_img = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-guestavatar');
1173 var chat_head_title_txt = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-guestname');
1174 var chat_head_title_bid = jQuery('<span></span>').addClass('vbo-modal-wguestmessages-chat-guestbid');
1175 var chat_head_open_bid = jQuery('<a></a>').addClass('badge badge-info').attr('href', 'JavaScript: void(0);').text(id).on('click', () => {
1176 VBOCore.handleDisplayWidgetNotification({
1177 widget_id: 'booking_details'
1178 }, {
1179 bid: id,
1180 modal_options: {
1181 suffix: 'widget_modal_inner_booking_details',
1182 }
1183 });
1184 });
1185 chat_head_title_bid.append(chat_head_open_bid);
1186
1187 var guest_avatar = message_el.find('.vbo-dashboard-guest-activity-avatar img');
1188 var guest_name = message_el.find('.vbo-w-guestmessages-message-gtitle').find('span').first().text();
1189 chat_head_title_txt.text(guest_name);
1190 if (guest_avatar && guest_avatar.length) {
1191 chat_head_title_img.append(guest_avatar.clone());
1192 chat_head_title.append(chat_head_title_img);
1193 }
1194 chat_head_title_top.append(chat_head_title_txt);
1195 chat_head_title_top.append(chat_head_title_bid);
1196
1197 chat_head_title_bot.append(message_el.find('.vbo-dashboard-guest-activity-content-info-icon').html());
1198
1199 // register callback for no-reply-needed click
1200 var no_reply_needed_el = jQuery('<a></a>').addClass('label').attr('href', 'JavaScript: void(0);').text(Joomla.JText._('VBO_NO_REPLY_NEEDED'));
1201 if (message_el.attr('data-noreply-needed') == 1) {
1202 // this thread was marked as no-reply needed
1203 no_reply_needed_el.addClass('label-danger');
1204 }
1205 no_reply_needed_el.on('click', () => {
1206 if (confirm(Joomla.JText._('VBO_WANT_PROCEED'))) {
1207 var id_thread = message_el.attr('data-idthread');
1208 if (!id_thread || !id_thread.length) {
1209 return false;
1210 }
1211
1212 // perform the request to toggle the thread as no-reply-needed
1213 var call_method = 'setNoReplyNeededThread';
1214
1215 VBOCore.doAjax(
1216 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1217 {
1218 widget_id: "<?php echo $this->getIdentifier(); ?>",
1219 call: call_method,
1220 bid: id,
1221 id_thread: id_thread,
1222 status: message_el.attr('data-noreply-needed'),
1223 tmpl: "component"
1224 },
1225 (response) => {
1226 try {
1227 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1228 if (!obj_res.hasOwnProperty(call_method)) {
1229 console.error('Unexpected JSON response', obj_res);
1230 return false;
1231 }
1232
1233 if (chat_rendering_meth == 'modal') {
1234 // emit the event to close (dismiss) the modal
1235 VBOCore.emitEvent(modal_dismiss_event);
1236
1237 // reload the widget
1238 vboWidgetGuestMessagesLoad(message_block.attr('id'));
1239 } else {
1240 // update data attribute for toggle without destroying the chat
1241 if (message_el.attr('data-noreply-needed') == 1) {
1242 message_el.attr('data-noreply-needed', 0);
1243 no_reply_needed_el.removeClass('label-danger');
1244 // remove the reply button
1245 message_el.find('.message-unreplied').remove();
1246 } else {
1247 message_el.attr('data-noreply-needed', 1);
1248 no_reply_needed_el.addClass('label-danger');
1249 }
1250 }
1251 } catch(err) {
1252 console.error('could not parse JSON response', err, response);
1253 }
1254 },
1255 (error) => {
1256 console.error(error);
1257 }
1258 );
1259 }
1260 });
1261
1262 // append element that will contain the listing details involved
1263 var listing_details_parent_el = jQuery('<span></span>').addClass('vbo-w-guestmessages-message-stayrooms');
1264 var listing_details_el = jQuery('<span></span>').addClass('vbo-w-guestmessages-message-listings').text('...');
1265 listing_details_parent_el.append(listing_details_el);
1266 chat_head_title_bot.append(listing_details_parent_el);
1267
1268 // append no-reply-needed element
1269 chat_head_title_bot.append(no_reply_needed_el);
1270
1271 // append additional elements
1272 chat_head_title_wrap.append(chat_head_title_top);
1273 chat_head_title_wrap.append(chat_head_title_bot);
1274
1275 // finalize title
1276 chat_head_title.append(chat_head_title_wrap);
1277
1278 if (chat_rendering_meth == 'modal') {
1279 // display modal
1280 var chat_modal_body = VBOCore.displayModal({
1281 suffix: 'wguestmessages-chat',
1282 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1283 title: chat_head_title,
1284 draggable: true,
1285 dismiss_event: modal_dismiss_event,
1286 onDismiss: () => {
1287 if (typeof VCMChat !== 'undefined') {
1288 VCMChat.getInstance().destroy();
1289 }
1290 },
1291 loading_event: modal_loading_event,
1292 });
1293
1294 // start loading
1295 VBOCore.emitEvent(modal_loading_event);
1296 } else {
1297 // append head to chat container
1298 let chat_inline_head = jQuery('<div></div>').addClass('vbo-w-guestmessages-inboxstyle-chat-head');
1299 chat_inline_head.append(chat_head_title);
1300 chat_inline_container.append(chat_inline_head);
1301
1302 // set inline loading body
1303 let chat_inline_loading = jQuery('<div></div>').addClass('vbo-w-guestmessages-inboxstyle-chat-loading');
1304 chat_inline_loading.html('<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>');
1305 chat_inline_container.append(chat_inline_loading);
1306 }
1307
1308 // perform the request to render the chat in the apposite modal or inline wrapper
1309 var call_method = 'renderChat';
1310
1311 VBOCore.doAjax(
1312 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1313 {
1314 widget_id: "<?php echo $this->getIdentifier(); ?>",
1315 call: call_method,
1316 bid: id,
1317 tmpl: "component"
1318 },
1319 (response) => {
1320 if (chat_rendering_meth == 'modal') {
1321 // stop loading
1322 VBOCore.emitEvent(modal_loading_event);
1323 } else {
1324 // unset inline loading body
1325 chat_inline_container.find('.vbo-w-guestmessages-inboxstyle-chat-loading').remove();
1326 }
1327
1328 try {
1329 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1330 if (!obj_res.hasOwnProperty(call_method)) {
1331 console.error('Unexpected JSON response', obj_res);
1332 return false;
1333 }
1334
1335 // append HTML code to render the chat to the apposite container
1336 if (chat_rendering_meth == 'modal') {
1337 chat_modal_body.html(obj_res[call_method]);
1338 } else {
1339 let chat_inline_body = jQuery('<div></div>').addClass('vbo-w-guestmessages-inboxstyle-chat-body');
1340 chat_inline_body.html(obj_res[call_method]);
1341 chat_inline_container.append(chat_inline_body);
1342 }
1343
1344 // register scroll to bottom with a small delay
1345 setTimeout(() => {
1346 if (typeof VCMChat !== 'undefined') {
1347 VCMChat.getInstance().scrollToBottom();
1348 }
1349 }, 150);
1350 } catch(err) {
1351 console.error('could not parse JSON response', err, response);
1352 }
1353 },
1354 (error) => {
1355 if (chat_rendering_meth == 'modal') {
1356 // stop loading
1357 VBOCore.emitEvent(modal_loading_event);
1358 } else {
1359 // empty inline chat container
1360 chat_inline_container.html('');
1361 }
1362
1363 // log and display error
1364 console.error(error);
1365 alert(error.responseText);
1366 }
1367 );
1368
1369 // perform the request to load the listing details involved
1370 VBOCore.doAjax(
1371 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1372 {
1373 widget_id: "<?php echo $this->getIdentifier(); ?>",
1374 call: 'loadListingDetails',
1375 return: 1,
1376 bid: id,
1377 tmpl: "component"
1378 },
1379 (response) => {
1380 try {
1381 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1382 if (!obj_res.hasOwnProperty('loadListingDetails')) {
1383 console.error('Unexpected JSON response', obj_res);
1384 return false;
1385 }
1386
1387 if (!Array.isArray(obj_res['loadListingDetails']['listings']) || !obj_res['loadListingDetails']['listings'].length) {
1388 // remove listing details element
1389 chat_head_title_bot.find('.vbo-w-guestmessages-message-stayrooms').remove();
1390
1391 return false;
1392 }
1393
1394 // set listing details
1395 chat_head_title_bot.find('.vbo-w-guestmessages-message-listings').html('<?php VikBookingIcons::e('home'); ?> ' + obj_res['loadListingDetails']['listings'].join(', '));
1396 } catch(err) {
1397 console.error('could not parse JSON response', err, response);
1398 }
1399 },
1400 (error) => {
1401 // log error
1402 console.error(error);
1403 }
1404 );
1405 }
1406
1407 /**
1408 * Display the loading skeletons.
1409 */
1410 function vboWidgetGuestMessagesSkeletons(wrapper, howmany) {
1411 let widget_instance = jQuery('#' + wrapper);
1412 if (!widget_instance.length) {
1413 return false;
1414 }
1415
1416 // number of skeletons to build
1417 let numSkeletons = <?php echo $this->messages_per_page; ?>;
1418 if (howmany && !isNaN(howmany) && howmany > 0) {
1419 numSkeletons = howmany;
1420 }
1421
1422 // determine the messages rendering method
1423 var mess_rendering_meth = widget_instance.hasClass('vbo-w-guestmessages-inboxstyle') ? 'inbox' : 'paging';
1424
1425 if (mess_rendering_meth != 'inbox') {
1426 // empty the current messages list
1427 widget_instance.find('.vbo-dashboard-guest-messages-list').html('');
1428 }
1429
1430 for (let i = 0; i < numSkeletons; i++) {
1431 // build skeleton element
1432 let skeleton = '';
1433 skeleton += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
1434 skeleton += ' <div class="vbo-dashboard-guest-activity-avatar">';
1435 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
1436 skeleton += ' </div>';
1437 skeleton += ' <div class="vbo-dashboard-guest-activity-content">';
1438 skeleton += ' <div class="vbo-dashboard-guest-activity-content-head">';
1439 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
1440 skeleton += ' </div>';
1441 skeleton += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
1442 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
1443 skeleton += ' </div>';
1444 skeleton += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
1445 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
1446 skeleton += ' </div>';
1447 skeleton += ' </div>';
1448 skeleton += '</div>';
1449
1450 // append skeleton element to the messages list
1451 widget_instance.find('.vbo-dashboard-guest-messages-list').append(skeleton);
1452 }
1453 }
1454
1455 /**
1456 * Perform the request to load the latest messages.
1457 */
1458 function vboWidgetGuestMessagesLoad(wrapper, bid_convo, resetting) {
1459 var widget_instance = jQuery('#' + wrapper);
1460 if (!widget_instance.length) {
1461 return false;
1462 }
1463
1464 var current_offset = parseInt(widget_instance.attr('data-offset'));
1465 var length_per_page = parseInt(widget_instance.attr('data-length'));
1466
1467 // determine the messages rendering method (paging or inbox, where "inbox" will likely never be on the first loading)
1468 var mess_rendering_meth = widget_instance.hasClass('vbo-w-guestmessages-inboxstyle') ? 'inbox' : 'paging';
1469
1470 // build search filter values
1471 var filters = {
1472 guest_name: widget_instance.find('input.vbo-widget-guest-messages-guestname').val(),
1473 message: widget_instance.find('input.vbo-widget-guest-messages-messcontains').val(),
1474 sender: widget_instance.find('input.vbo-widget-guest-messages-filter-sender[type="radio"]:checked').val(),
1475 fromdt: widget_instance.find('input.vbo-widget-guest-messages-fromdt').val(),
1476 todt: widget_instance.find('input.vbo-widget-guest-messages-todt').val(),
1477 unread: (widget_instance.find('input[name="unread"]').prop('checked') ? 1 : 0),
1478 ai_sort: (widget_instance.find('input[name="ai_sort"]').prop('checked') ? 1 : 0),
1479 };
1480
1481 // custom messages limit per page
1482 let limpage = widget_instance.find('input.vbo-widget-guest-messages-limpage').val();
1483 if (!isNaN(limpage) && limpage > 0 && limpage != length_per_page) {
1484 // update value
1485 length_per_page = limpage;
1486 // set new limit
1487 widget_instance.attr('data-length', limpage);
1488 }
1489
1490 // access the current messages list
1491 let messagesList = document
1492 .querySelector('#' + wrapper)
1493 .querySelector('.vbo-w-guestmessages-list-container');
1494
1495 // the widget method to call
1496 var call_method = 'loadMessages';
1497
1498 // make a request to load the messages
1499 VBOCore.doAjax(
1500 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1501 {
1502 widget_id: "<?php echo $this->getIdentifier(); ?>",
1503 call: call_method,
1504 return: 1,
1505 bid_convo: bid_convo,
1506 filters: filters,
1507 offset: current_offset,
1508 length: length_per_page,
1509 wrapper: wrapper,
1510 tmpl: "component"
1511 },
1512 (response) => {
1513 try {
1514 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1515 if (!obj_res.hasOwnProperty(call_method)) {
1516 console.error('Unexpected JSON response', obj_res);
1517 return false;
1518 }
1519
1520 if (mess_rendering_meth == 'inbox' && parseInt(current_offset) > 0) {
1521 // inbox style should get rid of non-needed elements and append the new messages
1522 widget_instance.find('.vbo-guestactivitywidget-commands').remove();
1523 widget_instance.find('.vbo-dashboard-guest-messages-list').find('.vbo-dashboard-guest-activity-skeleton').remove();
1524 widget_instance.find('.vbo-dashboard-guest-messages-list').find('.info[data-no-messages="1"]').remove();
1525 widget_instance.find('.vbo-dashboard-guest-messages-list').append(obj_res[call_method]['html']);
1526 if (parseInt(obj_res[call_method]['tot_messages']) > 0) {
1527 // move the class to the new last guest message
1528 widget_instance.find('.vbo-w-guestmessages-message.vbo-w-guestmessages-message-last').removeClass('vbo-w-guestmessages-message-last');
1529 widget_instance.find('.vbo-w-guestmessages-message').last().addClass('vbo-w-guestmessages-message-last');
1530 }
1531 } else {
1532 // replace HTML with new messages
1533 widget_instance.find('.vbo-dashboard-guest-messages-list').html(obj_res[call_method]['html']);
1534 }
1535
1536 if (mess_rendering_meth == 'inbox') {
1537 if (!messagesList.pageLoading && parseInt(current_offset) > 0) {
1538 // inbox style with messages not loaded through infinite scroll
1539 // on a page after the first, may indicate that we are on a very
1540 // long screen (high height) that did not detect the scrolling of
1541 // the messages list to be needed, so we trigger the resize event
1542 VBOCore.emitEvent('vbo-resize-widget-modal<?php echo $js_intvals_id; ?>');
1543 } else if (resetting) {
1544 // applying or cancelling filters may require to re-configure the infinite scroll events
1545 VBOCore.emitEvent('vbo-resize-widget-modal<?php echo $js_intvals_id; ?>');
1546 }
1547
1548 // always turn custom property off for the page loading
1549 messagesList.pageLoading = false;
1550 }
1551
1552 // check if latest datetime is set
1553 if (obj_res[call_method]['latest_dt']) {
1554 widget_instance.attr('data-latestdt', obj_res[call_method]['latest_dt']);
1555 }
1556
1557 // check results
1558 if (parseInt(obj_res[call_method]['tot_messages']) < 1) {
1559 // no results can indicate the offset is invalid or too high
1560 if (!isNaN(current_offset) && parseInt(current_offset) > 0) {
1561 if (mess_rendering_meth == 'inbox') {
1562 // inbox style should simply destroy the infinite scroll
1563 vboWidgetGuestMessagesDestroyInfiniteScroll(wrapper);
1564 } else {
1565 // reset offset to 0
1566 widget_instance.attr('data-offset', 0);
1567 // show loading skeletons
1568 vboWidgetGuestMessagesSkeletons(wrapper);
1569 // reload the first page
1570 vboWidgetGuestMessagesLoad(wrapper);
1571 }
1572 }
1573 } else {
1574 if (bid_convo) {
1575 // emit the event to read all notifications in the current context
1576 VBOCore.emitEvent('vbo-nc-read-notifications', {
1577 criteria: {
1578 group: 'guests',
1579 idorder: bid_convo,
1580 }
1581 });
1582 }
1583 }
1584 } catch(err) {
1585 console.error('could not parse JSON response', err, response);
1586 }
1587 },
1588 (error) => {
1589 // display and log the error
1590 alert(error.responseText);
1591 console.error(error);
1592
1593 if (mess_rendering_meth == 'inbox') {
1594 // turn custom property off for the page loading
1595 messagesList.pageLoading = false;
1596 }
1597
1598 // remove the skeleton loading
1599 widget_instance.find('.vbo-dashboard-guest-messages-list').find('.vbo-dashboard-guest-activity-skeleton').remove();
1600 }
1601 );
1602 }
1603
1604 /**
1605 * Navigate between the various pages of the messages.
1606 */
1607 function vboWidgetGuestMessagesNavigate(wrapper, direction) {
1608 var widget_instance = jQuery('#' + wrapper);
1609 if (!widget_instance.length) {
1610 return false;
1611 }
1612
1613 // current offset
1614 var current_offset = parseInt(widget_instance.attr('data-offset'));
1615
1616 // steps per type
1617 var steps = parseInt(widget_instance.attr('data-length'));
1618
1619 // determine the messages rendering method (paging or inbox, where "inbox" will likely never be on the first loading)
1620 var mess_rendering_meth = widget_instance.hasClass('vbo-w-guestmessages-inboxstyle') ? 'inbox' : 'paging';
1621
1622 if (mess_rendering_meth != 'inbox') {
1623 // show loading skeletons only when in chat "modal" style
1624 vboWidgetGuestMessagesSkeletons(wrapper);
1625 }
1626
1627 // check direction and update offset for next nav
1628 if (direction > 0) {
1629 // navigate forward
1630 widget_instance.attr('data-offset', (current_offset + steps));
1631 } else {
1632 // navigate backward
1633 var new_offset = current_offset - steps;
1634 new_offset = new_offset >= 0 ? new_offset : 0;
1635 widget_instance.attr('data-offset', new_offset);
1636 }
1637
1638 // launch navigation
1639 vboWidgetGuestMessagesLoad(wrapper);
1640 }
1641
1642 /**
1643 * Watch periodically if there are new messages to be displayed (inline rendering only).
1644 */
1645 function vboWidgetGuestMessagesWatch(wrapper) {
1646 var widget_instance = jQuery('#' + wrapper);
1647 if (!widget_instance.length) {
1648 return false;
1649 }
1650
1651 var latest_dt = widget_instance.attr('data-latestdt');
1652 if (!latest_dt || !latest_dt.length) {
1653 return false;
1654 }
1655
1656 // determine the messages rendering method (paging or inbox, where "inbox" will likely never be on the first loading)
1657 var mess_rendering_meth = widget_instance.hasClass('vbo-w-guestmessages-inboxstyle') ? 'inbox' : 'paging';
1658
1659 // the widget method to call
1660 var call_method = 'watchMessages';
1661
1662 // make a request to watch the messages
1663 VBOCore.doAjax(
1664 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
1665 {
1666 widget_id: "<?php echo $this->getIdentifier(); ?>",
1667 call: call_method,
1668 latest_dt: latest_dt,
1669 tmpl: "component"
1670 },
1671 (response) => {
1672 try {
1673 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1674 if (!obj_res.hasOwnProperty(call_method)) {
1675 console.error('Unexpected JSON response', obj_res);
1676 return false;
1677 }
1678
1679 // response will contain the number of new messages
1680 if (isNaN(obj_res[call_method]) || parseInt(obj_res[call_method]) < 1) {
1681 // do nothing
1682 return;
1683 }
1684
1685 if (mess_rendering_meth == 'inbox') {
1686 // do nothing when inbox-style, hence in a modal window, because this interval
1687 // will not run and new messages are taken from the apposite events dispatched
1688 // this statement should never verify
1689 } else {
1690 // new messages found, reset the offset and re-load the first page
1691 widget_instance.attr('data-offset', 0);
1692 // show loading skeletons
1693 vboWidgetGuestMessagesSkeletons(wrapper);
1694 // reload the first page
1695 vboWidgetGuestMessagesLoad(wrapper);
1696 }
1697 } catch(err) {
1698 console.error('could not parse JSON response', err, response);
1699 }
1700 },
1701 (error) => {
1702 // do nothing
1703 console.error(error);
1704 }
1705 );
1706 }
1707
1708 /**
1709 * Reloads chat messages when using inbox-style.
1710 */
1711 function vboWidgetGuestMessagesReloadInbox(wrapper) {
1712 let widget_instance = jQuery('#' + wrapper);
1713 if (!widget_instance.length) {
1714 return false;
1715 }
1716
1717 // check if the admin was typing a reply message
1718 let is_typing = false;
1719 try {
1720 let reply_message = jQuery(VCMChat.getInstance().data.element.inputBox).find('textarea').val();
1721 if (reply_message) {
1722 // turn flag on
1723 is_typing = true;
1724 }
1725 } catch(e) {
1726 // do nothing
1727 }
1728
1729 if (is_typing) {
1730 console.info('New messages not reloaded because the administrator is typing a reply to a guest message.');
1731 return false;
1732 }
1733
1734 // destroy inifite scroll events
1735 vboWidgetGuestMessagesDestroyInfiniteScroll(wrapper);
1736 // empty messages list
1737 widget_instance.find('.vbo-dashboard-guest-messages-list').html('');
1738 // reset offset to 0
1739 widget_instance.attr('data-offset', 0);
1740 // show loading skeletons
1741 vboWidgetGuestMessagesSkeletons(wrapper);
1742 // reload guest messages for this widget's instance
1743 vboWidgetGuestMessagesLoad(wrapper, null, true);
1744 // restore infinite scroll loading, if needed
1745 VBOCore.emitEvent('vbo-resize-widget-modal<?php echo $js_intvals_id; ?>');
1746 }
1747
1748 /**
1749 * Destroys the infinite scroll loading when using inbox-style.
1750 */
1751 function vboWidgetGuestMessagesDestroyInfiniteScroll(wrapper) {
1752 let messagesList = document
1753 .querySelector('#' + wrapper)
1754 .querySelector('.vbo-w-guestmessages-list-container');
1755
1756 // un-register infinite scroll event handler
1757 messagesList
1758 .removeEventListener('scroll', vboWidgetGuestMessagesInfiniteScroll);
1759 }
1760
1761 /**
1762 * Setups the infinite scroll loading when using inbox-style.
1763 */
1764 function vboWidgetGuestMessagesSetupInfiniteScroll(wrapper) {
1765 let messagesList = document
1766 .querySelector('#' + wrapper)
1767 .querySelector('.vbo-w-guestmessages-list-container');
1768
1769 // get wrapper dimensions
1770 let listViewHeight = messagesList.offsetHeight;
1771 let listGlobHeight = messagesList.scrollHeight;
1772
1773 if (listViewHeight >= listGlobHeight) {
1774 // no scrolling detected, show manual loading of the next page
1775 document
1776 .querySelector('#' + wrapper)
1777 .querySelector('.vbo-guestactivitywidget-commands')
1778 .style
1779 .display = 'block';
1780
1781 return;
1782 }
1783
1784 // inject custom property to identify the wrapper ID
1785 messagesList.wrapperId = wrapper;
1786
1787 // register infinite scroll event handler
1788 messagesList
1789 .addEventListener('scroll', vboWidgetGuestMessagesInfiniteScroll);
1790 }
1791
1792 /**
1793 * Infinite scroll event handler when using inbox-style.
1794 */
1795 function vboWidgetGuestMessagesInfiniteScroll(e) {
1796 // access the injected wrapper ID property
1797 let wrapper = e.currentTarget.wrapperId;
1798
1799 if (!wrapper) {
1800 return;
1801 }
1802
1803 // register throttling callback
1804 VBOCore.throttleTimer(() => {
1805 // access the current messages list
1806 let messagesList = document
1807 .querySelector('#' + wrapper)
1808 .querySelector('.vbo-w-guestmessages-list-container');
1809
1810 // make sure the loading of a next page isn't running
1811 if (messagesList.pageLoading) {
1812 // abort
1813 return;
1814 }
1815
1816 // get wrapper dimensions
1817 let listViewHeight = messagesList.offsetHeight;
1818 let listGlobHeight = messagesList.scrollHeight;
1819 let listScrollTop = messagesList.scrollTop;
1820
1821 if (!listScrollTop || listViewHeight >= listGlobHeight) {
1822 // no scrolling detected at all
1823 return;
1824 }
1825
1826 // calculate missing distance to the end of the list
1827 let listEndDistance = listGlobHeight - (listViewHeight + listScrollTop);
1828
1829 if (listEndDistance < <?php echo $this->px_distance_threshold; ?>) {
1830 // inject custom property to identify a next page is loading
1831 messagesList.pageLoading = true;
1832
1833 // show that we are loading more messages
1834 vboWidgetGuestMessagesSkeletons(wrapper, 3);
1835
1836 // load the next page of messages
1837 vboWidgetGuestMessagesNavigate(wrapper, 1);
1838 }
1839 }, 500);
1840 }
1841
1842 /**
1843 * Subscribe to the event emitted by VCM's chat handler when replying to a guest message.
1844 */
1845 document.addEventListener('vcm-guest-message-replied', (e) => {
1846 if (!e || !e.detail) {
1847 return;
1848 }
1849
1850 // pool of messaging elements to scan
1851 let elements = [];
1852
1853 // gather the supported event detail properties
1854 let idorder = e.detail.hasOwnProperty('idorder') ? e.detail['idorder'] : null;
1855 let idthread = e.detail.hasOwnProperty('idthread') ? e.detail['idthread'] : null;
1856 let idmessage = e.detail.hasOwnProperty('idmessage') ? e.detail['idmessage'] : null;
1857
1858 // check if some eligible elements can be fetched
1859 if (idorder) {
1860 elements = document.querySelectorAll('.vbo-w-guestmessages-message[data-idorder="' + idorder + '"]');
1861 } else if (idthread) {
1862 elements = document.querySelectorAll('.vbo-w-guestmessages-message[data-idthread="' + idthread + '"]');
1863 } else if (idmessage) {
1864 elements = document.querySelectorAll('.vbo-w-guestmessages-message[data-idmessage="' + idmessage + '"]');
1865 }
1866
1867 // scan all the elements from which the "reply" label should be removed, if any
1868 elements.forEach((element) => {
1869 let unreplied = element.querySelector('.message-unreplied');
1870 if (unreplied) {
1871 // remove node stating that the guest message needs a reply
1872 unreplied.remove();
1873 }
1874 });
1875 });
1876
1877 </script>
1878 <?php
1879 }
1880 ?>
1881
1882 <script type="text/javascript">
1883
1884 jQuery(function() {
1885
1886 // when document is ready, load latest messages for this widget's instance
1887 vboWidgetGuestMessagesLoad('<?php echo $wrapper_id; ?>', '<?php echo $bid_convo; ?>');
1888
1889 // make sure we've got no other chat instances on the same page (editorder)
1890 if (jQuery('#vbmessagingdiv').length) {
1891 if (typeof VCMChat !== 'undefined') {
1892 VCMChat.getInstance().destroy();
1893 }
1894 jQuery('#vbmessagingdiv').html('<p class="info"><?php echo JHtml::fetch('esc_attr', JText::translate('VBO_W_GUESTMESSAGES_TITLE')); ?> - <?php echo JHtml::fetch('esc_attr', JText::translate('VBO_MULTITASK_PANEL')); ?></p>');
1895 }
1896
1897 // render datepicker calendar for dates navigation
1898 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-messages-fromdt, .vbo-widget-guest-messages-todt').datepicker({
1899 maxDate: "+1d",
1900 yearRange: "<?php echo (date('Y') - 3); ?>:<?php echo date('Y'); ?>",
1901 changeMonth: true,
1902 changeYear: true,
1903 dateFormat: "<?php echo $this->getDateFormat('jui'); ?>",
1904 onSelect: function(selectedDate) {
1905 if (!selectedDate) {
1906 return;
1907 }
1908 if (jQuery(this).hasClass('vbo-widget-guest-messages-fromdt')) {
1909 let nowstart = jQuery(this).datepicker('getDate');
1910 let nowstartdate = new Date(nowstart.getTime());
1911 jQuery('.vbo-widget-guest-messages-todt').datepicker('option', {minDate: nowstartdate});
1912 }
1913 }
1914 });
1915
1916 // triggering for datepicker calendar icon
1917 jQuery('#<?php echo $wrapper_id; ?>').find('.vbo-widget-guest-messages-fromdt-trigger, .vbo-widget-guest-messages-todt-trigger').click(function() {
1918 var jdp = jQuery(this).parent().find('input.hasDatepicker');
1919 if (jdp.length) {
1920 jdp.focus();
1921 }
1922 });
1923
1924 // register callback function for the widget "resize" event (modal only)
1925 const resize_fn = (e) => {
1926 let inboxStyleThreshold = 1000;
1927
1928 let modalContent = e?.detail?.content ? (e?.detail?.content[0] || e?.detail?.content || null) : null;
1929 if (!modalContent || !modalContent.matches('#<?php echo $wrapper_id; ?>')) {
1930 modalContent = document.querySelector('#<?php echo $wrapper_id; ?>');
1931 }
1932
1933 // always attempt to destroy infinite scroll
1934 vboWidgetGuestMessagesDestroyInfiniteScroll('<?php echo $wrapper_id; ?>');
1935
1936 let overlayContent = modalContent.closest('.vbo-modal-overlay-content');
1937 let outerBody = modalContent.closest('.vbo-modal-overlay-content-body');
1938 if (!overlayContent || !outerBody) {
1939 // widget may be minimized on the admin-dock
1940 return;
1941 }
1942
1943 // get messages list container
1944 let listContainer = modalContent.querySelector('.vbo-w-guestmessages-list-container');
1945
1946 if (modalContent.offsetWidth > inboxStyleThreshold) {
1947 // enable inbox-style
1948 modalContent.classList.add('vbo-w-guestmessages-inboxstyle');
1949 overlayContent.classList.add('vbo-modal-body-no-scroll');
1950 // calculate the messages list container height to allow y-scrolling
1951 listContainer.style.height = (outerBody.offsetHeight - (listContainer.offsetTop - outerBody.offsetTop) - 4) + 'px';
1952 // set head-no-border class
1953 outerBody.classList.add('vbo-modal-head-no-border');
1954 // setup infinite scroll
1955 vboWidgetGuestMessagesSetupInfiniteScroll('<?php echo $wrapper_id; ?>');
1956 } else {
1957 // disable inbox-style
1958 modalContent.classList.remove('vbo-w-guestmessages-inboxstyle');
1959 overlayContent.classList.remove('vbo-modal-body-no-scroll');
1960 // reset the messages list container height property
1961 listContainer.style.height = 'initial';
1962 // reset head-no-border class
1963 outerBody.classList.remove('vbo-modal-head-no-border');
1964 }
1965 };
1966
1967 // register callback for the new guest messages event (modal only)
1968 const new_messages_fn = (e) => {
1969 if (!e || !e.detail || !e.detail?.messages) {
1970 return;
1971 }
1972
1973 let modalContent = document.querySelector('#<?php echo $wrapper_id; ?>');
1974 if (!modalContent || !Array.isArray(e.detail.messages) || !e.detail.messages.length) {
1975 return;
1976 }
1977
1978 let modalWrap = modalContent.closest('.vbo-modal-widget_modal-wrap[data-dock-minimized]');
1979 if (!modalWrap) {
1980 return;
1981 }
1982
1983 // check if the widget is minimized
1984 if (modalWrap.getAttribute('data-dock-minimized') == 1) {
1985 // register self-destructive event for reloading the inbox upon widget restoring from admin-dock
1986 document.addEventListener('vbo-admin-dock-restore-<?php echo $this->getIdentifier(); ?>', function vboWidgetGuestMessagesRestoreNReload(e) {
1987 // make sure the same event won't propagate again
1988 e.target.removeEventListener(e.type, vboWidgetGuestMessagesRestoreNReload);
1989
1990 // reload chat messages
1991 vboWidgetGuestMessagesReloadInbox('<?php echo $wrapper_id; ?>');
1992 });
1993 } else {
1994 // reload chat messages
1995 vboWidgetGuestMessagesReloadInbox('<?php echo $wrapper_id; ?>');
1996 }
1997 };
1998
1999 <?php
2000 if ($js_intvals_id) {
2001 // widget can be dismissed or "resized" through the modal
2002 ?>
2003 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_intvals_id; ?>', (e) => {
2004 if (typeof watch_intv !== 'undefined') {
2005 // clear interval for notifications
2006 clearInterval(watch_intv);
2007 }
2008
2009 if (jQuery('#vbmessagingdiv').length) {
2010 // reload the page for the previously removed chat in the editorder page
2011 location.reload();
2012 }
2013
2014 // get rid of widget resizing events
2015 document.removeEventListener('vbo-resize-widget-modal<?php echo $js_intvals_id; ?>', resize_fn);
2016 document.removeEventListener('vbo-admin-dock-restore-<?php echo $this->getIdentifier(); ?>', resize_fn);
2017 window.removeEventListener('resize', resize_fn);
2018
2019 // get rid of new guest messages event
2020 document.removeEventListener('vbo-new-guest-messages', new_messages_fn);
2021
2022 // if there was an inline chat, destroy it
2023 if (typeof VCMChat !== 'undefined') {
2024 VCMChat.getInstance().destroy();
2025 }
2026 });
2027
2028 // register widget resizing events
2029 document.addEventListener('vbo-resize-widget-modal<?php echo $js_intvals_id; ?>', resize_fn);
2030 document.addEventListener('vbo-admin-dock-restore-<?php echo $this->getIdentifier(); ?>', resize_fn);
2031 window.addEventListener('resize', resize_fn);
2032
2033 /**
2034 * Subscribe to the event emitted when new guest messages are received.
2035 */
2036 document.addEventListener('vbo-new-guest-messages', new_messages_fn);
2037 <?php
2038 } else {
2039 // set interval for loading new messages automatically (only when not modal rendering)
2040 ?>
2041 const watch_intv = setInterval(function() {
2042 vboWidgetGuestMessagesWatch('<?php echo $wrapper_id; ?>');
2043 }, 60000);
2044 <?php
2045 }
2046 ?>
2047
2048 });
2049
2050 </script>
2051
2052 <?php
2053 }
2054 }
2055