PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
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 / site / layouts / tools / guest_messaging.php

guest_messaging.php in VikBooking Hotel Booking Engine & PMS 1.8.16, at site/layouts/tools/guest_messaging.php

732 lines 25.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 * Obtain vars from arguments received in the layout file.
15 * This is the layout file for the "guest_messaging" operator tool.
16 *
17 * @var string $tool The tool identifier.
18 * @var array $operator The operator record accessing the tool.
19 * @var object $permissions The operator-tool permissions registry.
20 * @var string $tool_uri The base URI for rendering this tool.
21 */
22 extract($displayData);
23
24 // messages per page
25 $messages_per_page = 20;
26
27 // distance threshold in pixels between the current scroll position and the end of the list
28 $px_distance_threshold = 140;
29
30 // get listings assigned to the current operator (no listings equals to all listings)
31 $listings = array_filter(
32 array_map('intval', (array) $permissions->get('rooms', []))
33 );
34
35 // attempt to require the chat handler from VCM
36 try {
37 VikBooking::getVcmChatInstance($oid = 0, $channel = null);
38 } catch (Throwable $e) {
39 // propagate the error code
40 VBOHttpDocument::getInstance()->close($e->getCode(), 'Channel Manager not available.');
41 }
42
43 // make sure VCM is available and up to date
44 if (!class_exists('VCMChatHandler') || !method_exists('VCMChatHandler', 'loadChatAssets')) {
45 // raise an error
46 VBOHttpDocument::getInstance()->close(500, 'Channel Manager not available.');
47 }
48
49 /**
50 * Load the VBOCore JS class and chat assets from VCM.
51 */
52 VikBooking::getVboApplication()->loadCoreJS();
53 VCMChatHandler::loadChatAssets();
54
55 // load JS lang definitions
56 JText::script('VBTODAY');
57 JText::script('VBSTANDBY');
58 JText::script('VBCONFIRMED');
59 JText::script('VBCANCELLED');
60 JText::script('VBOCHECKEDSTATUSIN');
61 JText::script('VBOCHECKEDSTATUSOUT');
62 JText::script('VBOCHECKEDSTATUSNOS');
63 JText::script('VBO_REPLY');
64 JText::script('VBO_NO_MORE_MESSAGES');
65 JText::script('VBO_NO_REPLY_NEEDED');
66 JText::script('VBO_WANT_PROCEED');
67 JText::script('VBO_NEW_MESSAGES');
68
69 ?>
70 <div class="vbo-optool-gmessaging-wrap">
71 <div class="vbo-optool-gmessaging-inner">
72 <div class="vbo-optool-gmessaging-list-container" data-offset="0">
73 <?php
74 for ($i = 0; $i < $messages_per_page; $i++) {
75 ?>
76 <div class="vbo-optool-gmessaging-guest-activity vbo-optool-gmessaging-guest-activity-skeleton">
77 <div class="vbo-optool-gmessaging-guest-activity-avatar">
78 <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>
79 </div>
80 <div class="vbo-optool-gmessaging-guest-activity-content">
81 <div class="vbo-optool-gmessaging-guest-activity-content-head">
82 <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>
83 </div>
84 <div class="vbo-optool-gmessaging-guest-activity-content-info-msg">
85 <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>
86 </div>
87 </div>
88 </div>
89 <?php
90 }
91 ?>
92 </div>
93 <div class="vbo-optool-gmessaging-inboxstyle-chat"></div>
94 </div>
95 </div>
96
97 <script type="text/javascript">
98
99 const vbo_media_base_uri = '<?php echo VBO_SITE_URI . 'resources/uploads/'; ?>';
100
101 /**
102 * Loads and renders the chat for a given booking ID.
103 *
104 * @note do NOT use an arrow function because the clicked message is passed as "thisArg".
105 */
106 const vbo_optool_gmessaging_load_chat_fn = function(bid) {
107 // define the clicked message
108 let clickedMessage = this;
109
110 // gather thread id
111 let threadId = 0;
112 try {
113 threadId = clickedMessage.getAttribute('data-idthread');
114 } catch(err) {
115 // silently catch the error
116 console.error(err);
117 }
118
119 // empty current chat
120 let chatContainer = document.querySelector('.vbo-optool-gmessaging-inboxstyle-chat');
121 if (typeof VCMChat !== 'undefined') {
122 VCMChat.getInstance().destroy();
123 }
124 chatContainer.innerHTML = '';
125
126 // start loading
127 let loadingElement = document.createElement('div');
128 loadingElement.classList.add('vbo-optool-gmessaging-chat-loading');
129 loadingElement.innerHTML = '<span><?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?></span>';
130 chatContainer.append(loadingElement);
131
132 // make the request to the controller
133 VBOCore.doAjax(
134 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.renderGuestBookingChat') ?>",
135 {
136 bid: bid,
137 },
138 (response) => {
139 response = typeof response === 'string' ? JSON.parse(response) : response;
140
141 // remove loading element
142 loadingElement.remove();
143
144 // create chat head element
145 let chatHead = document.createElement('div');
146 chatHead.classList.add('vbo-optool-gmessaging-inboxstyle-chat-head');
147
148 // build chat head-inner element
149 let chatHeadInner = document.createElement('div');
150 try {
151 // clone and set avatar
152 let guestAvatar = clickedMessage.querySelector('.vbo-optool-gmessaging-message-avatar').cloneNode(true);
153 chatHeadInner.append(guestAvatar);
154
155 // build head details element
156 let chatHeadDetails = document.createElement('div');
157 chatHeadDetails.classList.add('vbo-optool-gmessaging-inboxstyle-chat-head-details');
158
159 // get guest name
160 let guestName = clickedMessage.querySelector('.vbo-optool-gmessaging-message-guestname-txt').innerText;
161
162 // build head info element
163 let chatHeadInfo = document.createElement('div');
164 chatHeadInfo.classList.add('vbo-optool-gmessaging-inboxstyle-chat-head-info');
165
166 // append guest name
167 let guestNameEl = document.createElement('span');
168 guestNameEl.classList.add('vbo-optool-gmessaging-inboxstyle-chat-head-info-gname');
169 guestNameEl.innerText = guestName;
170 chatHeadInfo.append(guestNameEl);
171
172 // append booking ID
173 let bidEl = document.createElement('span');
174 bidEl.classList.add('vbo-badge');
175 bidEl.classList.add('badge-info');
176 bidEl.classList.add('vbo-optool-gmessaging-inboxstyle-chat-head-info-bid');
177 bidEl.innerText = bid;
178 chatHeadInfo.append(bidEl);
179
180 // set head info
181 chatHeadDetails.append(chatHeadInfo);
182
183 // build head summary element
184 let chatHeadSummary = document.createElement('div');
185 chatHeadSummary.classList.add('vbo-optool-gmessaging-inboxstyle-chat-head-summary');
186
187 // clone and build booking summary
188 let bookingSummary = clickedMessage.querySelector('.vbo-optool-gmessaging-message-bookinginfo').cloneNode(true);
189
190 // listing details
191 let listingDetails = document.createElement('span');
192 listingDetails.classList.add('vbo-optool-gmessaging-message-listings');
193 bookingSummary.append(listingDetails);
194 vbo_optool_gmessaging_set_listingdets_fn(bid, listingDetails);
195
196 // no-reply needed
197 let noReplyNeededEl = document.createElement('a');
198 noReplyNeededEl.classList.add('vbo-label');
199 noReplyNeededEl.classList.add('vbo-optool-gmessaging-message-noreplyneeded');
200 noReplyNeededEl.innerText = Joomla.JText._('VBO_NO_REPLY_NEEDED');
201 noReplyNeededEl.addEventListener('click', () => {
202 if (confirm(Joomla.JText._('VBO_WANT_PROCEED'))) {
203 // get no-reply-needed status on thread
204 let threadNoReplyNeededStatus = clickedMessage ? (clickedMessage.getAttribute('data-noreply-needed') || 0) : 0;
205
206 // toggle no-reply-needed status on thread
207 VBOCore.doAjax(
208 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.toggleGuestThreadNoReplyNeeded') ?>",
209 {
210 bid: bid,
211 status: threadNoReplyNeededStatus,
212 id_thread: threadId,
213 },
214 (response) => {
215 if (threadNoReplyNeededStatus == 1) {
216 noReplyNeededEl.classList.remove('label-danger');
217 clickedMessage.setAttribute('data-noreply-needed', '0');
218 } else {
219 noReplyNeededEl.classList.add('label-danger');
220 clickedMessage.setAttribute('data-noreply-needed', '1');
221 }
222 },
223 (error) => {
224 console.error(error);
225 }
226 );
227 }
228 });
229 bookingSummary.append(noReplyNeededEl);
230
231 // set booking summary
232 chatHeadSummary.append(bookingSummary);
233
234 // set head summary
235 chatHeadDetails.append(chatHeadSummary);
236
237 // set the whole head details block
238 chatHeadInner.append(chatHeadDetails);
239 } catch(err) {
240 // silently catch the error
241 console.error(err);
242 }
243
244 // append chat head
245 chatHead.append(chatHeadInner);
246 chatContainer.append(chatHead);
247
248 // create chat body element
249 let chatBody = document.createElement('div');
250 chatBody.classList.add('vbo-optool-gmessaging-inboxstyle-chat-body');
251
252 // append chat body
253 chatContainer.append(chatBody);
254
255 // render chat through jQuery
256 jQuery('.vbo-optool-gmessaging-inboxstyle-chat-body').html(response.html);
257 // rendering through pure JS will NOT work
258 // chatContainer.setHTMLUnsafe(response['html']);
259
260 // register scroll to bottom with a small delay
261 setTimeout(() => {
262 if (typeof VCMChat !== 'undefined') {
263 VCMChat.getInstance().scrollToBottom();
264 }
265 }, 150);
266 },
267 (error) => {
268 // log and display error
269 console.error(error);
270 alert(error.responseText || 'Unknown error');
271 // abort loading
272 loadingElement.remove();
273 }
274 );
275 };
276
277 /**
278 * Loads the listing details for a booking.
279 */
280 const vbo_optool_gmessaging_set_listingdets_fn = (bid, detailsNode) => {
281 // perform the request
282 VBOCore.doAjax(
283 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.loadGuestBookingListings') ?>",
284 {
285 bid: bid,
286 },
287 (response) => {
288 response = typeof response === 'string' ? JSON.parse(response) : response;
289
290 if (!detailsNode) {
291 detailsNode = document.querySelector('.vbo-optool-gmessaging-message-listings');
292 }
293
294 detailsNode.innerHTML = '<?php VikBookingIcons::e('home'); ?> ' + response.listings.join(', ');
295 },
296 (error) => {
297 console.error(error);
298 }
299 );
300 };
301
302 /**
303 * Loads a pagination of thread messages.
304 */
305 const vbo_optool_gmessaging_load_threads_fn = (offset, limit) => {
306 // validate offset and limit
307 offset = parseInt((isNaN(offset) ? 0 : offset));
308 limit = parseInt((isNaN(limit) || limit < 1 ? <?php echo $messages_per_page; ?> : limit));
309
310 // perform the request
311 VBOCore.doAjax(
312 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.loadGuestThreads') ?>",
313 {
314 start: offset,
315 limit: limit,
316 },
317 (response) => {
318 let threads = typeof response === 'string' ? JSON.parse(response) : response;
319 let container = document.querySelector('.vbo-optool-gmessaging-list-container');
320 let messagesList = container.querySelector('.vbo-optool-gmessaging-messages-list');
321 let lastMessage = container.querySelector('.vbo-optool-gmessaging-message-last');
322
323 if (lastMessage && threads.length) {
324 // remove the "last message" class
325 lastMessage.classList.remove('vbo-optool-gmessaging-message-last');
326 }
327
328 if (!offset) {
329 // first page loading will clean up the container with the skeletons
330 container.innerHTML = '';
331
332 if (threads.length) {
333 // first page loading should also set the most recent message (thread) date
334 container.setAttribute('data-last-updated', (threads[0]?.last_updated || ''));
335 }
336 } else {
337 // scan and remove all skeletons
338 container.querySelectorAll('.vbo-optool-gmessaging-guest-activity-skeleton').forEach((skeleton) => {
339 skeleton.remove();
340 });
341 }
342
343 // update offset for next page loading
344 container.setAttribute('data-offset', offset + limit);
345
346 if (!threads.length) {
347 if (offset > 0) {
348 // attempt to destroy the infinite scroll loading
349 vbo_optool_gmessaging_destroy_infinite_scroll_fn();
350 }
351
352 // display the "no more messages" element
353 let noMessagesEl = document.createElement('p');
354 noMessagesEl.classList.add('info');
355 noMessagesEl.setAttribute('data-no-messages', '1');
356 noMessagesEl.innerText = Joomla.JText._('VBO_NO_MORE_MESSAGES');
357
358 if (messagesList && !messagesList.querySelector('.info[data-no-messages="1"]')) {
359 // no more messages
360 messagesList.append(noMessagesEl);
361 } else if (!messagesList) {
362 // no messages at all
363 container.append(noMessagesEl);
364 }
365 }
366
367 if (!messagesList) {
368 // create the messages list element
369 messagesList = document.createElement('div');
370 messagesList.classList.add('vbo-optool-gmessaging-messages-list');
371 } else {
372 // turn custom property off for the page loading
373 container.pageLoading = false;
374 }
375
376 // scan all thread messages
377 threads.forEach((thread, index) => {
378 // create message element
379 let messageElement = document.createElement('div');
380 messageElement.classList.add('vbo-optool-gmessaging-message');
381
382 if (!thread?.read_dt && (thread?.sender_type + '').toLowerCase() == 'guest') {
383 messageElement.classList.add('vbo-optool-gmessaging-message-new');
384 }
385
386 if (thread?.replied == 0 && (thread?.sender_type + '').toLowerCase() == 'guest') {
387 messageElement.classList.add('vbo-optool-gmessaging-message-unreplied');
388 }
389
390 if (!offset && !index) {
391 messageElement.classList.add('vbo-optool-gmessaging-message-first');
392 } else if (offset && (index + 1) == threads.length) {
393 messageElement.classList.add('vbo-optool-gmessaging-message-last');
394 }
395
396 // set attributes
397 messageElement.setAttribute('data-idorder', thread.idorder);
398 messageElement.setAttribute('data-idthread', thread.id_thread);
399 messageElement.setAttribute('data-idmessage', thread.id_message);
400 messageElement.setAttribute('data-noreply-needed', (thread?.no_reply_needed || 0));
401
402 // register click event
403 messageElement.addEventListener('click', () => {
404 document.querySelectorAll('.vbo-optool-gmessaging-message-active').forEach((activeMess) => {
405 activeMess.classList.remove('vbo-optool-gmessaging-message-active');
406 });
407 vbo_optool_gmessaging_load_chat_fn.call(messageElement, thread.idorder);
408 messageElement.classList.add('vbo-optool-gmessaging-message-active');
409 messageElement.classList.remove('vbo-optool-gmessaging-message-new');
410 messageElement.classList.remove('vbo-optool-gmessaging-message-unreplied');
411 let unrepliedIcn = messageElement.querySelector('.message-unreplied');
412 if (unrepliedIcn) {
413 unrepliedIcn.remove();
414 }
415 });
416
417 // build message avatar
418 let avatarPic = '';
419 let avatarPicEl = document.createElement('div');
420 let messageAvatar = document.createElement('div');
421 messageAvatar.classList.add('vbo-optool-gmessaging-message-avatar');
422 if (thread?.pic) {
423 avatarPic = thread.pic.indexOf('http') === 0 ? thread.pic : vbo_media_base_uri + thread.pic;
424 } else if (thread?.channel_logo) {
425 avatarPic = thread.channel_logo;
426 }
427 if (!avatarPic) {
428 avatarPicEl.classList.add('vbo-optool-gmessaging-message-avatar-icon');
429 avatarPicEl.innerHTML = '<?php VikBookingIcons::e('user'); ?>';
430 } else {
431 let avatarPicImg = document.createElement('img');
432 avatarPicImg.setAttribute('src', avatarPic);
433 avatarPicEl.classList.add('vbo-optool-gmessaging-message-avatar-profile');
434 avatarPicEl.append(avatarPicImg);
435 }
436 messageAvatar.append(avatarPicEl);
437
438 // build message content elements
439 let messageContent = document.createElement('div');
440 messageContent.classList.add('vbo-optool-gmessaging-message-content');
441 let messContHead = document.createElement('div');
442 messContHead.classList.add('vbo-optool-gmessaging-message-content-head');
443
444 // message content head details
445 let messHeadDetails = document.createElement('div');
446 messHeadDetails.classList.add('vbo-optool-gmessaging-message-content-details');
447 let messGuestName = document.createElement('div');
448 messGuestName.classList.add('vbo-optool-gmessaging-message-guestname');
449 let fullGuestName = ((thread?.first_name || '') + ' ' + (thread?.last_name || '')).trim();
450 if (!fullGuestName.length) {
451 fullGuestName = 'Guest';
452 }
453 let fullGuestNameEl = document.createElement('span');
454 fullGuestNameEl.classList.add('vbo-optool-gmessaging-message-guestname-txt');
455 fullGuestNameEl.innerText = fullGuestName;
456 messGuestName.append(fullGuestNameEl);
457 if (thread?.replied == 0 && (thread?.sender_type + '').toLowerCase() == 'guest') {
458 let unrepliedEl = document.createElement('span');
459 unrepliedEl.classList.add('label');
460 unrepliedEl.classList.add('label-small');
461 unrepliedEl.classList.add('message-unreplied');
462 unrepliedEl.innerHTML = '<?php VikBookingIcons::e('comments', 'message-reply') ?> ' + Joomla.JText._('VBO_REPLY');
463 messGuestName.append(unrepliedEl);
464 }
465 messHeadDetails.append(messGuestName);
466 let messageBookInfo = document.createElement('div');
467 messageBookInfo.classList.add('vbo-optool-gmessaging-message-bookinginfo');
468 let bookStatus = document.createElement('span');
469 bookStatus.classList.add('vbo-badge');
470 if (thread?.b_status) {
471 if (thread.b_status == 'standby') {
472 bookStatus.classList.add('badge-warning');
473 bookStatus.innerText = Joomla.JText._('VBSTANDBY');
474 } else if (thread.b_status == 'cancelled') {
475 bookStatus.classList.add('badge-danger');
476 bookStatus.innerText = Joomla.JText._('VBCANCELLED');
477 } else {
478 bookStatus.classList.add('badge-success');
479 if (thread?.b_checkout && thread.b_checkout < thread.message_info.current_ts) {
480 bookStatus.innerText = Joomla.JText._('VBOCHECKEDSTATUSOUT');
481 } else {
482 bookStatus.innerText = Joomla.JText._('VBCONFIRMED');
483 }
484 }
485 }
486 messageBookInfo.append(bookStatus);
487 let stayDates = document.createElement('span');
488 stayDates.classList.add('vbo-optool-gmessaging-message-staydates');
489 let stayDatesIn = document.createElement('span');
490 stayDatesIn.classList.add('vbo-optool-gmessaging-message-stayin');
491 stayDatesIn.innerText = thread.message_info.str_checkin;
492 let stayDatesSep = document.createElement('span');
493 stayDatesSep.classList.add('vbo-optool-gmessaging-message-staysep');
494 stayDatesSep.innerText = ' - ';
495 let stayDatesOut = document.createElement('span');
496 stayDatesOut.classList.add('vbo-optool-gmessaging-message-stayout');
497 stayDatesOut.innerText = thread.message_info.str_checkout;
498 stayDates.append(stayDatesIn);
499 stayDates.append(stayDatesSep);
500 stayDates.append(stayDatesOut);
501 messageBookInfo.append(stayDates);
502 messHeadDetails.append(messageBookInfo);
503
504 // message content head date
505 let messHeadDate = document.createElement('div');
506 messHeadDate.classList.add('vbo-optool-gmessaging-message-content-date');
507 let messHeadTimeSpan = document.createElement('span');
508 messHeadTimeSpan.innerText = thread.message_info.time;
509 messHeadDate.append(messHeadTimeSpan);
510 let messHeadDateSpan = document.createElement('span');
511 messHeadDateSpan.innerText = thread.message_info.is_today ? Joomla.JText._('VBTODAY') : thread.message_info.date;
512 messHeadDate.append(messHeadDateSpan);
513
514 // finalize head element
515 messContHead.append(messHeadDetails);
516 messContHead.append(messHeadDate);
517
518 // message content text element
519 let messContMess = document.createElement('div');
520 messContMess.classList.add('vbo-optool-gmessaging-message-content-msg');
521 let messContMsg = document.createElement('p');
522 messContMsg.innerHTML = (thread.content + '').substring(0, 90);
523 if ((thread.content + '').length > 90) {
524 messContMsg.innerText = messContMsg.innerText + '...';
525 }
526 messContMess.append(messContMsg);
527
528 // append message content elements
529 messageContent.append(messContHead);
530 messageContent.append(messContMess);
531
532 // append message elements
533 messageElement.append(messageAvatar);
534 messageElement.append(messageContent);
535
536 // append message element to list
537 messagesList.append(messageElement);
538 });
539
540 if (!container.querySelector('.vbo-optool-gmessaging-messages-list')) {
541 // messages list element must be appended to main container
542 container.append(messagesList);
543 }
544
545 if (!offset && threads.length) {
546 // attempt to register the infinite scroll loading
547 setTimeout(() => {
548 vbo_optool_gmessaging_setup_infinite_scroll_fn();
549 }, 150);
550 }
551 },
552 (error) => {
553 // log and display error
554 console.error(error);
555 alert(error.responseText || 'Unknown error');
556 }
557 );
558 };
559
560 /**
561 * Destroys the infinite scroll loading.
562 */
563 const vbo_optool_gmessaging_destroy_infinite_scroll_fn = () => {
564 let messagesList = document.querySelector('.vbo-optool-gmessaging-list-container');
565
566 // un-register infinite scroll event handler
567 messagesList.removeEventListener('scroll', vbo_optool_gmessaging_handle_infinite_scroll_fn);
568 };
569
570 /**
571 * Sets up the infinite scroll loading.
572 */
573 const vbo_optool_gmessaging_setup_infinite_scroll_fn = () => {
574 let messagesList = document.querySelector('.vbo-optool-gmessaging-list-container');
575
576 // get wrapper dimensions
577 let listViewHeight = messagesList.offsetHeight;
578 let listGlobHeight = messagesList.scrollHeight;
579
580 if (listViewHeight >= listGlobHeight) {
581 // no scrolling detected, probably because of too few messages
582 return;
583 }
584
585 // register infinite scroll event handler
586 messagesList.addEventListener('scroll', vbo_optool_gmessaging_handle_infinite_scroll_fn);
587 };
588
589 /**
590 * Handles the infinite scroll loading.
591 */
592 const vbo_optool_gmessaging_handle_infinite_scroll_fn = (e) => {
593 // register throttling callback
594 VBOCore.throttleTimer(() => {
595 // access the current messages container
596 let messagesList = document.querySelector('.vbo-optool-gmessaging-list-container');
597
598 // make sure the loading of a next page isn't running
599 if (messagesList.pageLoading) {
600 // abort
601 return;
602 }
603
604 // get wrapper dimensions
605 let listViewHeight = messagesList.offsetHeight;
606 let listGlobHeight = messagesList.scrollHeight;
607 let listScrollTop = messagesList.scrollTop;
608
609 if (!listScrollTop || listViewHeight >= listGlobHeight) {
610 // no scrolling detected at all
611 return;
612 }
613
614 // calculate missing distance to the end of the list
615 let listEndDistance = listGlobHeight - (listViewHeight + listScrollTop);
616
617 if (listEndDistance < <?php echo $px_distance_threshold; ?>) {
618 // inject custom property to identify a next page is loading
619 messagesList.pageLoading = true;
620
621 // show that we are loading more messages
622 vbo_optool_gmessaging_draw_skeletons_fn(3);
623
624 // load the next page of messages
625 vbo_optool_gmessaging_load_more_fn();
626 }
627 }, 500);
628 };
629
630 /**
631 * Draws skeletons in the messages list container.
632 */
633 const vbo_optool_gmessaging_draw_skeletons_fn = (num) => {
634 let messagesPool = document.querySelector('.vbo-optool-gmessaging-messages-list');
635 if (!messagesPool) {
636 return;
637 }
638
639 num = parseInt((isNaN(num) || num < 1 ? 3 : num));
640
641 for (let i = 0; i < num; i++) {
642 let skeleton = '';
643 skeleton += '<div class="vbo-optool-gmessaging-guest-activity vbo-optool-gmessaging-guest-activity-skeleton">';
644 skeleton += ' <div class="vbo-optool-gmessaging-guest-activity-avatar">';
645 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
646 skeleton += ' </div>';
647 skeleton += ' <div class="vbo-optool-gmessaging-guest-activity-content">';
648 skeleton += ' <div class="vbo-optool-gmessaging-guest-activity-content-head">';
649 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
650 skeleton += ' </div>';
651 skeleton += ' <div class="vbo-optool-gmessaging-guest-activity-content-info-msg">';
652 skeleton += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
653 skeleton += ' </div>';
654 skeleton += ' </div>';
655 skeleton += '</div>';
656
657 messagesPool.insertAdjacentHTML('beforeend', skeleton);
658 }
659 };
660
661 /**
662 * Loads the next page of messages.
663 */
664 const vbo_optool_gmessaging_load_more_fn = () => {
665 // access the current messages container
666 let messagesList = document.querySelector('.vbo-optool-gmessaging-list-container');
667
668 // get the current offset to load a new page of messages
669 let offset = messagesList.getAttribute('data-offset');
670
671 // load the next page of thread messages
672 vbo_optool_gmessaging_load_threads_fn(offset, <?php echo $messages_per_page; ?>);
673 };
674
675 /**
676 * Checks for new thread messages.
677 */
678 const vbo_optool_gmessaging_check_new_threads_fn = () => {
679 let container = document.querySelector('.vbo-optool-gmessaging-list-container');
680 let last_date = container.getAttribute('data-last-updated');
681
682 // perform the request
683 VBOCore.doAjax(
684 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.checkNewGuestThreads') ?>",
685 {
686 last_date: last_date,
687 },
688 (response) => {
689 response = typeof response === 'string' ? JSON.parse(response) : response;
690
691 if (response.newThreads.length) {
692 if (!container.querySelector('.info[data-new-messages="1"]')) {
693 // display message
694 let newMessEl = document.createElement('p');
695 newMessEl.classList.add('info');
696 newMessEl.setAttribute('data-new-messages', '1');
697
698 let reloadLink = document.createElement('a');
699 reloadLink.setAttribute('href', '<?php echo $tool_uri; ?>');
700 reloadLink.innerText = Joomla.JText._('VBO_NEW_MESSAGES');
701
702 newMessEl.append(reloadLink);
703
704 container.prepend(newMessEl);
705 }
706
707 // update attribute
708 container.setAttribute('data-last-updated', (response.newThreads[0]?.last_updated || last_date));
709 }
710 },
711 (error) => {
712 console.error(error);
713 }
714 );
715 };
716
717 /**
718 * Configure the DOM-ready execution.
719 */
720 jQuery(function() {
721
722 // load the first page of threads on page load
723 vbo_optool_gmessaging_load_threads_fn(0, <?php echo $messages_per_page; ?>);
724
725 // register a periodic interval to watch for new messages
726 setInterval(() => {
727 vbo_optool_gmessaging_check_new_threads_fn();
728 }, 120000);
729
730 });
731 </script>
732