PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.12
VikBooking Hotel Booking Engine & PMS v1.8.12
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.12, at admin/helpers/widgets/guest_messages.php

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