widgetName = JText::translate('VBO_W_GUESTMESSAGES_TITLE'); $this->widgetDescr = JText::translate('VBO_W_GUESTMESSAGES_DESCR'); $this->widgetId = basename(__FILE__, '.php'); // define widget and icon and style name $this->widgetIcon = ''; $this->widgetStyleName = 'light-orange'; // load widget's settings $this->widgetSettings = $this->loadSettings(); if (!is_object($this->widgetSettings)) { $this->widgetSettings = new stdClass; } // today Y-m-d date $this->today_ymd = date('Y-m-d'); // the path to the VCM library $this->vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php'; // whether VCM is available if (is_file($this->vcm_lib_path)) { if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getLatestFromGuests')) { // VCM is outdated $this->vcm_exists = false; } // attempt to require the chat handler try { VikBooking::getVcmChatInstance($oid = 0, $channel = null); } catch (Exception $e) { // do nothing } // make sure VCM is up to date for this widget if (!class_exists('VCMChatHandler') || !method_exists('VCMChatHandler', 'loadChatAssets')) { // VCM is outdated (>= 1.8.11 required) $this->vcm_exists = false; } } else { $this->vcm_exists = false; } // avoid queries on certain pages, as VCM may not have been activated yet if (VBOPlatformDetection::isWordPress() && $this->vcm_exists) { global $pagenow; if (isset($pagenow) && in_array($pagenow, ['update.php', 'plugins.php', 'plugin-install.php'])) { $this->vcm_exists = false; } } } /** * Preload the necessary CSS/JS assets from VCM. * * @return void */ public function preload() { if ($this->vcm_exists) { // load chat assets from VCM VCMChatHandler::loadChatAssets(); // datepicker calendar $this->vbo_app->loadDatePicker(); // additional language defs JText::script('VBO_NO_REPLY_NEEDED'); JText::script('VBO_WANT_PROCEED'); JText::script('VBOSIGNATURECLEAR'); JText::script('VBODASHSEARCHKEYS'); } } /** * @inheritDoc * * @since 1.17.6 (J) - 1.7.6 (WP) */ public function getWidgetDetails() { // get common widget details from parent abstract class $details = parent::getWidgetDetails(); // append the modal rendering information $details['modal'] = [ 'add_class' => 'vbo-modal-large', ]; return $details; } /** * Custom method for this widget only to load the latest guest messages. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * an associative array is returned thanks to the request value "return":1. * * It's the actual rendering of the widget which also allows navigation. */ public function loadMessages() { $input = JFactory::getApplication()->input; $bid_convo = $input->getString('bid_convo', ''); $filters = $input->get('filters', [], 'array'); $offset = $input->getInt('offset', 0); $length = $input->getInt('length', $this->messages_per_page); $wrapper = $input->getString('wrapper', ''); if (!$this->vcm_exists) { VBOHttpDocument::getInstance()->close(500, 'Vik Channel Manager is either not available or outdated'); } // check messages custom limit per page if ($length > 0 && $length != $this->messages_per_page) { // update widget settings $this->widgetSettings->limpage = $length; $this->updateSettings(json_encode($this->widgetSettings)); } // build search filters $search_filters = [ 'guest_name' => $filters['guest_name'] ?? '', 'message' => $filters['message'] ?? '', 'sender' => $filters['sender'] ?? '', 'fromdt' => $filters['fromdt'] ?? '', 'todt' => $filters['todt'] ?? '', 'unread' => (int) ($filters['unread'] ?? 0), 'ai_sort' => (int) ($filters['ai_sort'] ?? 0), ]; // filter out empty search filter values $search_filters = array_filter($search_filters); if (!empty($search_filters['fromdt'])) { // convert the date string from local format to military $search_filters['fromdt'] = date('Y-m-d H:i:s', VikBooking::getDateTimestamp($search_filters['fromdt'], 0, 0, 0)); // convert date from local timezone to UTC $search_filters['fromdt'] = JFactory::getDate($search_filters['fromdt'], date_default_timezone_get())->format('Y-m-d H:i:s'); } if (!empty($search_filters['todt'])) { // convert the date string from local format to military $search_filters['todt'] = date('Y-m-d H:i:s', VikBooking::getDateTimestamp($search_filters['todt'], 23, 59, 59)); // convert date from local timezone to UTC $search_filters['todt'] = JFactory::getDate($search_filters['todt'], date_default_timezone_get())->format('Y-m-d H:i:s'); } // initiate the chat messaging object $chat_messaging = class_exists('VCMChatMessaging') ? VCMChatMessaging::getInstance() : null; // last error description $latest_error = ''; // load latest messages $latest_messages = []; try { /** * Search filters require an updated VCM version. * * @since 1.16.9 (J) - 1.6.9 (WP) * @requires VCM >= 1.8.27 (1.9.5 "unread", "ai_sort") */ if ($search_filters && $chat_messaging && method_exists($chat_messaging, 'searchMessages')) { // search for specific messages with the specified search filters $latest_messages = $chat_messaging->searchMessages($search_filters, $offset, $length); } else { // regular loading of the latest guest messages with no search filters $latest_messages = VikChannelManager::getLatestFromGuests(['guest_messages'], $offset, $length); } } catch (Exception $e) { // do nothing, but populate the error description $latest_error = $e->getMessage(); } // the multitask data and notifications can request a specific conversation to be opened $bubble_convo = null; if ($bid_convo) { // make sure the requested booking ID was fetched from the most recent guest messages foreach ($latest_messages as $gmessage) { if ($bid_convo == $gmessage->idorder) { // specific conversation to bubble found $bubble_convo = $bid_convo; break; } if ($bid_convo == $gmessage->idorderota && strcasecmp((string)$gmessage->channel, 'vikbooking')) { // specific OTA conversation to bubble found // make sure to use the internal booking ID rather than the OTA booking ID $bid_convo = $gmessage->idorder ?: $bid_convo; // request conversation to be popped up $bubble_convo = $bid_convo; break; } } if (!$bubble_convo && $chat_messaging) { // updated VCM versions will allow us to fetch one conversation by booking ID $booking_messages = $chat_messaging->loadBookingGuestThreads($bid_convo); if ($booking_messages) { // append the requested conversation so that it will bubble $latest_messages = array_merge($latest_messages, $booking_messages); // turn flag on $bubble_convo = $bid_convo; } } } // current year Y and timestamp $current_y = date('Y'); $current_ts = time(); // start output buffering ob_start(); if ($latest_error) { ?>

0 ? JText::translate('VBO_NO_MORE_MESSAGES') : JText::translate('VBO_NO_RECORDS_FOUND'); ?>

$gmessage) { $gmessage_content = $gmessage->content; if (empty($gmessage_content)) { if (!empty($gmessage->attachments)) { $gmessage_content = ' ' . basename($gmessage->attachments[0] ?? ''); } else { $gmessage_content = '.....'; } } elseif (strlen($gmessage_content) > 90) { if (function_exists('mb_substr')) { $gmessage_content = mb_substr($gmessage_content, 0, 90, 'UTF-8'); } else { $gmessage_content = substr($gmessage_content, 0, 90); } $gmessage_content .= '...'; } // build extra classes for main element $wrap_classes = []; if ($ind === 0) { $wrap_classes[] = 'vbo-w-guestmessages-message-first'; } elseif ($ind == ($tot_messages - 1)) { $wrap_classes[] = 'vbo-w-guestmessages-message-last'; } if (empty($gmessage->read_dt) && !strcasecmp($gmessage->sender_type, 'guest')) { $wrap_classes[] = 'vbo-w-guestmessages-message-new'; } ?>
guest_avatar)) { // highest priority goes to the profile picture, not always available ?> pic)) { // customer profile picture is not the same as the photo avatar ?> channel_logo)) { // channel logo goes as second option ?> channel_logo)) { ?> ai_priority)) { // display AI-calculated priority icon and apposite class (expected "high", "medium" or "low") $priority_icon = 'flag'; $priority_class = preg_replace("/[^a-z]/", '', strtolower((string) $gmessage->ai_priority)); $priority_value = ucwords((string) $gmessage->ai_priority); if (!strcasecmp(trim((string) $gmessage->ai_priority), 'high')) { $priority_value = JText::translate('VBO_PRIORITY_HIGH'); } elseif (!strcasecmp(trim((string) $gmessage->ai_priority), 'medium')) { $priority_value = JText::translate('VBO_PRIORITY_MEDIUM'); } elseif (!strcasecmp(trim((string) $gmessage->ai_priority), 'low')) { $priority_value = JText::translate('VBO_PRIORITY_LOW'); } ?>
0) { // show backward navigation button ?>
0) { $page_number = floor($offset / $length) + 1; $page_number = $page_number > 0 ? $page_number : 1; } ?>
0 && $offset === 0 ? $latest_messages[0]->last_updated : null; // get the HTML buffer $html_content = ob_get_contents(); ob_end_clean(); // return an associative array of values return [ 'html' => $html_content, 'tot_messages' => $tot_messages, 'offset' => ($offset + $length), 'latest_dt' => $latest_datetime, ]; } /** * Custom method for this widget only to watch the latest guest messages. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * * Outputs the new number of messages found from the latest datetime. */ public function watchMessages() { $latest_dt = VikRequest::getString('latest_dt', '', 'request'); if (empty($latest_dt)) { echo '0'; return; } if (!$this->vcm_exists) { VBOHttpDocument::getInstance()->close(500, 'Vik Channel Manager is either not available or outdated'); } // load the latest guest message (one is sufficient) $latest_messages = []; try { $latest_messages = VikChannelManager::getLatestFromGuests(['guest_messages'], 0, 1); } catch (Exception $e) { // do nothing } if (!$latest_messages || $latest_messages[0]->last_updated == $latest_dt) { // no newest messages found echo '0'; return; } // print 1 to indicate that new messages should be reloaded echo '1'; } /** * Custom method for this widget only to render the chat of a booking. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * no values should be returned, as the response must be sent to output * in case the JS/CSS assets will be echoed within the response. * * Returns the necessary HTML code to render the chat. */ public function renderChat() { $app = JFactory::getApplication(); $bid = $app->input->getUint('bid', 0); $threadId = $app->input->getUint('threadid', 0); $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find booking'); } // initialize chat instance by getting the proper channel name if (empty($booking['channel'])) { // front-end reservation chat handler $chat_channel = 'vikbooking'; } else { $channelparts = explode('_', $booking['channel']); // check if this is a meta search channel $is_meta_search = false; if (preg_match("/(customer).*[0-9]$/", $channelparts[0]) || !strcasecmp($channelparts[0], 'googlehotel') || !strcasecmp($channelparts[0], 'googlevr') || !strcasecmp($channelparts[0], 'trivago')) { $is_meta_search = empty($booking['idorderota']); } if ($is_meta_search) { // customer of type sales channel should use front-end reservation chat handler $chat_channel = 'vikbooking'; } else { // let the getInstance method validate the channel chat handler $chat_channel = $booking['channel']; } } $messaging = VikBooking::getVcmChatInstance($booking['id'], $chat_channel); if (is_null($messaging)) { VBOHttpDocument::getInstance()->close(500, 'Could not render chat'); } // send content to output echo $messaging->renderChat([ 'hideThreads' => 1, 'preferredThread' => $threadId, ], $load_assets = false); } /** * Custom method for this widget only to update the thread of a booking. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * no values should be returned, as the response must be sent to output. * * Returns a successful string or throws an error. */ public function setNoReplyNeededThread() { $bid = VikRequest::getInt('bid', 0, 'request'); $id_thread = VikRequest::getInt('id_thread', 0, 'request'); $status = VikRequest::getInt('status', 0, 'request'); $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking || empty($id_thread)) { VBOHttpDocument::getInstance()->close(404, 'Could not find booking thread'); } // build thread object for update $thread = new stdClass; $thread->id = $id_thread; $thread->idorder = $bid; $thread->no_reply_needed = !$status ? 1 : 0; $dbo = JFactory::getDbo(); if (!$dbo->updateObject('#__vikchannelmanager_threads', $thread, ['id', 'idorder'])) { VBOHttpDocument::getInstance()->close(500, 'Could not update thread'); } echo '1'; } /** * Custom method for this widget only to load the listing details for the booking. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * an associative array is returned thanks to the request value "return":1. * * Needed to provide additional information to the host about the booked listing(s). */ public function loadListingDetails() { $bid = VikRequest::getInt('bid', 0, 'request'); $booking_rooms = VikBooking::loadOrdersRoomsData($bid); if (!$this->vcm_exists || !$booking_rooms) { VBOHttpDocument::getInstance()->close(500, 'Could not obtain the listing information'); } return [ 'listings' => array_column($booking_rooms, 'room_name'), ]; } public function render(?VBOMultitaskData $data = null) { // increase widget's instance counter static::$instance_counter++; // check whether the widget is being rendered via AJAX when adding it through the customizer $is_ajax = $this->isAjaxRendering(); // generate a unique ID for the guest messages wrapper instance $wrapper_instance = !$is_ajax ? static::$instance_counter : rand(); $wrapper_id = 'vbo-widget-guest-messages-' . $wrapper_instance; // this widget will work only if VCM is available and updated, and if permissions are met $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking'); if (!$this->vcm_exists || !$vbo_auth_bookings) { return; } // multitask data event identifier for clearing intervals $js_intvals_id = ''; $wrap_extra_class = ''; $bid_convo = 0; $unread_filter = false; $sort_by_ai = false; $contains_filter = ''; if ($data && $data->isModalRendering()) { // access Multitask data $js_intvals_id = $data->getModalJsIdentifier(); // check if a specific conversation should be opened if ($data->get('id_message', 0)) { $bid_convo = $data->getBookingId(); } else { $bid_convo = $this->options()->fetchBookingId(); } // set an extra class $wrap_extra_class = ' vbo-w-guestmessages-wrapmodal'; // check for unread messages filter $unread_filter = (bool) $this->options()->get('unread', ''); // check for AI sort filter $sort_by_ai = (bool) $this->options()->get('ai_sort', ''); // check if a specific filter was set for searching guest messages $contains_filter = (string) $this->options()->get('message_contains', ''); // check custom limit per page only when in modal rendering if (($this->widgetSettings->limpage ?? 0) > 0) { // set custom limit from widget settings $this->messages_per_page = (int) $this->widgetSettings->limpage; } // when modal rendering, we expect to adopt the inbox-style by default unless in small screens if ($this->messages_per_page < $this->inbox_messages_per_page) { // in order to facilitate the infinite scroll, we use the minimum number of messages per page $this->messages_per_page = $this->inbox_messages_per_page; } // check for limit filter injected $limpage_filter = (int) $this->options()->get('limpage', 0); if ($limpage_filter > 0) { // set custom limit $this->messages_per_page = $limpage_filter; } } ?>

widgetIcon; ?> widgetName; ?>

messages_per_page; $i++) { ?>