<?php
/**
 * @package     VikBooking
 * @subpackage  com_vikbooking
 * @author      Alessio Gaggii - e4j - Extensionsforjoomla.com
 * @copyright   Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE
 * @link        https://vikwp.com
 */

defined('ABSPATH') or die('No script kiddies please!');

/**
 * Class handler for admin widget "latest from guests".
 * 
 * @since 	1.14.0 (J) - 1.4.0 (WP)
 */
class VikBookingAdminWidgetLatestFromGuests extends VikBookingAdminWidget
{
	/**
	 * The instance counter of this widget.
	 *
	 * @var 	int
	 */
	protected static $instance_counter = -1;

	/**
	 * Number of activities per page. Should be an even number.
	 * 
	 * @var 	int
	 */
	protected $activities_per_page = 6;

	/**
	 * Number of activities per type. Should be half of numer per page.
	 * 
	 * @var 	int
	 */
	protected $activities_per_type = 3;

	/**
	 * Today Y-m-d string
	 * 
	 * @var 	string
	 */
	protected $today_ymd = null;

	/**
	 * The path to the VCM lib to see if it's available.
	 * 
	 * @var 	string
	 */
	protected $vcm_lib_path = '';

	/**
	 * Tells whether VCM is installed and updated.
	 * 
	 * @var 	bool
	 */
	protected $vcm_exists = true;

	/**
	 * List of new guest messages detected.
	 * 
	 * @var 	array
	 * 
	 * @since 	1.17.6 (J) - 1.7.6 (WP)
	 */
	protected $new_guest_messages = [];

	/**
	 * Class constructor will define the widget name and identifier.
	 */
	public function __construct()
	{
		// call parent constructor
		parent::__construct();

		$this->widgetName = JText::translate('VBO_W_LATESTFROMGUESTS_TITLE');
		$this->widgetDescr = JText::translate('VBO_W_LATESTFROMGUESTS_DESCR');
		$this->widgetId = basename(__FILE__, '.php');

		/**
		 * Define widget and icon and style name.
		 * 
		 * @since 	1.15.0 (J) - 1.5.0 (WP)
		 */
		$this->widgetIcon = '<i class="' . VikBookingIcons::i('comments') . '"></i>';
		$this->widgetStyleName = 'green';

		// activities per type is half the total number of activities
		$this->activities_per_type = ceil($this->activities_per_page / 2);

		// 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
		$this->vcm_exists = true;
		if (is_file($this->vcm_lib_path)) {
			// require the dependencies
			if (!class_exists('VikChannelManager')) {
				require_once $this->vcm_lib_path;
			}
			if (!method_exists('VikChannelManager', 'getLatestFromGuests')) {
				// VCM is outdated
				$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;
			$skip_pages = ['update.php', 'plugins.php', 'plugin-install.php'];
			if (isset($pagenow) && in_array($pagenow, $skip_pages)) {
				$this->vcm_exists = false;
			}
		}
	}

	/**
	 * This widget returns the latest activity information to schedule
	 * periodic watch data in order to be able to trigger notifications.
	 * No CSS/JS assets are needed during preloading.
	 * 
	 * @return 	void|object
	 */
	public function preload()
	{
		if (!$this->vcm_exists) {
			return null;
		}

		// use VCM to load the latest guest activity ids
		$latest_activities = [];
		try {
			$latest_activities = VikChannelManager::getLatestFromGuests(['messages', 'reviews'], 0, 1);
		} catch (Exception $e) {
			// do nothing
		}

		if (!is_array($latest_activities) || !$latest_activities) {
			return null;
		}

		// default watch-data values to monitor
		$watch_data = new stdClass;
		$watch_data->guest_review  = 0;
		$watch_data->guest_message = '';
		$watch_data->message_id    = 0;
		$watch_data->message_bid   = 0;

		foreach ($latest_activities as $activity) {
			if (isset($activity->id_review) && empty($watch_data->guest_review)) {
				// this is a guest review
				$watch_data->guest_review = $activity->id_review;
			} elseif (!isset($activity->id_review) && empty($watch_data->message_id)) {
				// this is a guest message
				$watch_data->guest_message = $activity->content;
				$watch_data->message_id    = $activity->id_message ?? 0;
				$watch_data->message_bid   = $activity->idorder;
			}
		}

		return $watch_data;
	}

	/**
	 * Checks for new notifications by using the previous preloaded watch-data.
	 * 
	 * @param 	?VBONotificationWatchdata 	$watch_data 	the preloaded watch-data object.
	 * 
	 * @return 	array 						data object to watch next and notifications array.
	 * 
	 * @see 	preload()
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	public function getNotifications(?VBONotificationWatchdata $watch_data = null)
	{
		// default empty values
		$watch_next    = null;
		$notifications = null;

		if (!$this->vcm_exists || !$watch_data) {
			return [$watch_next, $notifications];
		}

		// grab latest watch data values
		$latest_guest_review  = (int)$watch_data->get('guest_review', 0);
		$latest_guest_message = $watch_data->get('guest_message', '');
		$latest_message_id    = (int)$watch_data->get('message_id', 0);
		$latest_message_bid   = (int)$watch_data->get('message_bid', 0);

		// use VCM to load the latest guest activity ids
		try {
			$latest_activities = VikChannelManager::getLatestFromGuests(['messages', 'reviews'], 0, 1);
		} catch (Exception $e) {
			// do nothing
			$latest_activities = [];
		}

		if (!$latest_activities) {
			return [$watch_next, $notifications];
		}

		// default watch-data next values to monitor
		$watch_next = new stdClass;
		$watch_next->guest_review  = $latest_guest_review;
		$watch_next->guest_message = $latest_guest_message;
		$watch_next->message_id    = $latest_message_id;
		$watch_next->message_bid   = $latest_message_bid;

		// new notifications pool (if any)
		$notifications = [];

		foreach ($latest_activities as $activity) {
			if (isset($activity->id_review)) {
				// this is a guest review
				if ($activity->id_review > $latest_guest_review) {
					// set the next watch data value
					$watch_next->guest_review = $activity->id_review;
					// compose the notification(s) to dispatch for the guest review(s)
					$review_notifications = VBONotificationScheduler::getInstance()->buildReviewDataObjects([$activity]);
					if ($review_notifications) {
						$notifications = array_merge($notifications, $review_notifications);
					}
				}
			} else {
				// this is a guest message
				if (isset($activity->sender_type) && (!strcasecmp($activity->sender_type, 'host') || !strcasecmp($activity->sender_type, 'hotel'))) {
					// we don't want threads started by the host/hotel
					continue;
				}

				// make sure this is a more recent activity than the last one
				if ($activity->id_message > $latest_message_id && ($activity->idorder != $latest_message_bid || $activity->content != $latest_guest_message)) {
					// overwrite properties to watch at the next scheduled interval
					$watch_next->guest_message = $activity->content;
					$watch_next->message_id    = $activity->id_message;
					$watch_next->message_bid   = $activity->idorder;

					// make sure the same message was not previously dispatched through a Push notification
					if (!$watch_data->matchPushedGuestMessage($activity)) {
						// compose the notification(s) to dispatch for the guest message(s)
						$guestmess_notifications = VBONotificationScheduler::getInstance()->buildGuestMessageDataObjects([$activity]);
						if ($guestmess_notifications) {
							$notifications = array_merge($notifications, $guestmess_notifications);
						}

						// set the new guest message
						$this->new_guest_messages[] = [
							'id'  => $watch_next->message_id,
							'bid' => $watch_next->message_bid,
						];
					}
				}
			}
		}

		return [$watch_next, $notifications];
	}

	/**
	 * Checks for new events to be dispatched by using the previous preloaded watch-data.
	 * This method is called after getNotifications(), so we avoid querying the DB.
	 * 
	 * @param 	?VBONotificationWatchdata 	$watch_data 	the preloaded watch-data object.
	 * 
	 * @return 	array 						list of event objects to dispatch, if any.
	 * 
	 * @see 	preload()
	 * 
	 * @since 	1.17.6 (J) - 1.7.6 (WP)
	 */
	public function getNotificationEvents(?VBONotificationWatchdata $watch_data = null)
	{
		if (!$this->new_guest_messages) {
			return [];
		}

		// return the notification events to dispatch
		return [
			// propagate the new guest messages
			'vbo-new-guest-messages' => [
				'messages' => $this->new_guest_messages,
			],
			// update the admin-dock badge count, if widget available
			'vbo-admin-dock-update-badge' => [
				'widgetId'   => 'guest_messages',
				'badgeCount' => count($this->new_guest_messages),
			],
		];
	}

	/**
	 * Custom method for this widget only to load the latest guest activities.
	 * 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.
	 * 
	 * It's the actual rendering of the widget which also allows navigation.
	 */
	public function loadActivities()
	{
		$offset = VikRequest::getInt('offset', 0, 'request');
		$length = VikRequest::getInt('length', $this->activities_per_type, 'request');
		$wrapper = VikRequest::getString('wrapper', '', 'request');

		if (!$this->vcm_exists) {
			echo 'e4j.error.missing or outdated VCM';
			return;
		}

		// load latest activities
		$latest_activities = [];
		try {
			$latest_activities = VikChannelManager::getLatestFromGuests(['messages', 'reviews'], $offset, $length);
		} catch (Exception $e) {
			// do nothing
		}

		foreach ($latest_activities as $activity) {
			$activity_type = isset($activity->id_review) ? 'review' : 'message';
			$activity_content = $activity->content;
			if (empty($activity_content)) {
				$activity_content = '.....';
			} elseif (strlen($activity_content) > 90) {
				if (function_exists('mb_substr')) {
					$activity_content = mb_substr($activity_content, 0, 90, 'UTF-8');
				} else {
					$activity_content = substr($activity_content, 0, 90);
				}
				$activity_content .= '...';
			}
			// determine sender type in case of guest message
			$sender = 'Hotel';
			if ($activity_type == 'message' && isset($activity->sender_type)) {
				if (strcasecmp($activity->sender_type, 'Hotel') && strcasecmp($activity->sender_type, 'Host')) {
					$sender = 'Guest';
				}
			}

			?>
			<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-<?php echo $activity_type; ?>" onclick="vboWidgetLatestFromGuestsShowDetails('<?php echo $activity_type; ?>', '<?php echo $activity_type == 'message' ? $activity->idorder : $activity->id_review; ?>');">
				<div class="vbo-dashboard-guest-activity-avatar">
				<?php
				if (!empty($activity->guest_avatar)) {
					// highest priority goes to the profile picture, not always available
					?>
					<img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $activity->guest_avatar; ?>" />
					<?php
				} elseif (!empty($activity->pic)) {
					// customer profile picture is not the same as the photo avatar
					?>
					<img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo strpos($activity->pic, 'http') === 0 ? $activity->pic : VBO_SITE_URI . 'resources/uploads/' . $activity->pic; ?>" />
					<?php
				} elseif (!empty($activity->channel_logo)) {
					// channel logo goes as second option
					?>
					<img class="vbo-dashboard-guest-activity-avatar-profile" src="<?php echo $activity->channel_logo; ?>" />
					<?php
				} else {
					// we use an icon as fallback
					VikBookingIcons::e('user', 'vbo-dashboard-guest-activity-avatar-icon');
				}
				?>
				</div>
				<div class="vbo-dashboard-guest-activity-content">
					<div class="vbo-dashboard-guest-activity-content-head">
						<div class="vbo-dashboard-guest-activity-content-info-details">
							<h4><?php echo $activity->first_name . (!empty($activity->last_name) ? ' ' . $activity->last_name : ''); ?></h4>
							<div class="vbo-dashboard-guest-activity-content-info-icon">
							<?php
							if ($activity_type == 'review') {
								// we display an icon as well as the score
								VikBookingIcons::e('star');
								?>
								<span class="vbo-dashboard-guest-activity-content-info-rate"><?php echo round(($activity->score / 2), 1); ?></span>
								<?php
							} else {
								// we use just an icon to tell that it's a chat guest message
								if (empty($activity->read_dt) && $sender === 'Guest') {
									// print also an icon to inform that the message was not read
									VikBookingIcons::e('exclamation-circle');
									echo ' ';
								}
								VikBookingIcons::e('comment-dots');
							}
							?>
							</div>
						</div>
						<div class="vbo-dashboard-guest-activity-content-info-date">
						<?php
						$activity_ts = strtotime($activity->last_updated);
						?>
							<span><?php echo date('H:i', $activity_ts); ?></span>
						<?php
						if (date('Y-m-d', $activity_ts) != $this->today_ymd) {
							// format and print the date
							?>
							<span><?php echo date(str_replace('/', $this->datesep, $this->df), $activity_ts); ?></span>
							<?php
						} else {
							// print "today"
							?>
							<span><?php echo JText::translate('VBTODAY'); ?></span>
							<?php
						}
						?>
						</div>
					</div>
					<div class="vbo-dashboard-guest-activity-content-info-msg">
						<p><?php echo $activity_content; ?></p>
					</div>
				</div>
			</div>
			<?php
		}

		// append navigation
		?>
		<div class="vbo-guestactivitywidget-commands">
			<div class="vbo-guestactivitywidget-commands-main">
			<?php
			if ($offset > 0) {
				// show backward navigation button
				?>
				<div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-prev">
					<span class="vbo-guestactivitywidget-prev" onclick="vboWidgetLatestFromGuestsNavigate('<?php echo $wrapper; ?>', -1);"><?php VikBookingIcons::e('chevron-left'); ?></span>
				</div>
				<?php
			}
			?>
				<div class="vbo-guestactivitywidget-command-chevron vbo-guestactivitywidget-command-next">
					<span class="vbo-guestactivitywidget-next" onclick="vboWidgetLatestFromGuestsNavigate('<?php echo $wrapper; ?>', 1);"><?php VikBookingIcons::e('chevron-right'); ?></span>
				</div>
			</div>
		</div>
		<?php

		// append the total number of activities displayed, the current offset and the latest activity datetime
		$tot_activities  = count($latest_activities);
		$latest_datetime = $tot_activities > 0 && $offset === 0 ? $latest_activities[0]->last_updated : null;

		echo ';' . __FUNCTION__ . ';' . $tot_activities . ';' . __FUNCTION__ . ';' . $offset . ';' . __FUNCTION__ . ';' . $latest_datetime;
	}

	/**
	 * Custom method for this widget only to watch the latest guest activities.
	 * 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 activities found from the latest datetime.
	 */
	public function watchActivities()
	{
		$latest_dt = VikRequest::getString('latest_dt', '', 'request');
		if (empty($latest_dt)) {
			echo '0';
			return;
		}

		if (!$this->vcm_exists) {
			echo 'e4j.error.missing or outdated VCM';
			return;
		}

		// load the latest activity (one is sufficient)
		$latest_activities = [];
		try {
			$latest_activities = VikChannelManager::getLatestFromGuests(['messages', 'reviews'], 0, 1);
		} catch (Exception $e) {
			// do nothing
		}

		if (!$latest_activities || $latest_activities[0]->last_updated == $latest_dt) {
			// no newest activities found
			echo '0';
			return;
		}

		// print 1 to indicate that new activities should be reloaded
		echo '1';
	}

	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 latest from guests wrapper instance
		$wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
		$wrapper_id = 'vbo-widget-latest-from-guests-' . $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 intervals
		$js_intvals_id = '';
		if ($data && $data->isModalRendering()) {
			$js_intvals_id = $data->getModalJsIdentifier();
		}

		?>
		<div class="vbo-admin-widget-wrapper">
			<div class="vbo-admin-widget-head">
				<h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
			</div>
			<div id="<?php echo $wrapper_id; ?>" class="vbo-dashboard-guests-latest" data-offset="0" data-length="<?php echo $this->activities_per_type; ?>" data-latestdt="">
				<div class="vbo-dashboard-guest-activities-inner">
					<div class="vbo-dashboard-guest-activities-list">
					<?php
					for ($i = 0; $i < $this->activities_per_page; $i++) {
						?>
						<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">
							<div class="vbo-dashboard-guest-activity-avatar">
								<div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>
							</div>
							<div class="vbo-dashboard-guest-activity-content">
								<div class="vbo-dashboard-guest-activity-content-head">
									<div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>
								</div>
								<div class="vbo-dashboard-guest-activity-content-subhead">
									<div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>
								</div>
								<div class="vbo-dashboard-guest-activity-content-info-msg">
									<div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>
								</div>
							</div>
						</div>
						<?php
					}
					?>
					</div>
				</div>
			</div>
		</div>

		<?php
		if (static::$instance_counter === 0 || $is_ajax) {
			// HTML helper tag for URL routing and some JS functions should be loaded once per widget instance
			$admin_file_base = VBOPlatformDetection::isWordPress() ? 'admin.php' : 'index.php';
		?>
		<a class="vbo-widget-latest-from-guests-basenavuri" href="<?php echo $admin_file_base; ?>?option=com_vikbooking&task=editorder&cid[]=%d" style="display: none;"></a>

		<script type="text/javascript">

			/**
			 * Open the booking details page for the clicked activity
			 */
			function vboWidgetLatestFromGuestsOpenBooking(id, url_suffix) {
				var open_url = jQuery('.vbo-widget-latest-from-guests-basenavuri').first().attr('href');
				open_url = open_url.replace('%d', id) + url_suffix;
				// navigate
				document.location.href = open_url;
			}

			/**
			 * Shows the details of either a message or a review.
			 */
			function vboWidgetLatestFromGuestsShowDetails(type, id) {
				if (type === 'message') {
					VBOCore.handleDisplayWidgetNotification({
						widget_id: 'guest_messages'
					}, {
						bid: id,
						modal_options: {
							suffix: 'widget_modal_inner_guest_messages',
						}
					});
				} else if (type === 'review') {
					VBOCore.handleDisplayWidgetNotification({
						widget_id: 'guest_reviews'
					}, {
						review_id: id,
						modal_options: {
							suffix: 'widget_modal_inner_guest_reviews',
						}
					});
				} else {
					vboWidgetLatestFromGuestsOpenBooking(id, '');
				}
			}

			/**
			 * Display the loading skeletons.
			 */
			function vboWidgetLatestFromGuestsSkeletons(wrapper) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}
				widget_instance.find('.vbo-dashboard-guest-activities-list').html('');
				for (var i = 0; i < <?php echo $this->activities_per_page; ?>; i++) {
					var skeleton = '';
					skeleton += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
					skeleton += '	<div class="vbo-dashboard-guest-activity-avatar">';
					skeleton += '		<div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
					skeleton += '	</div>';
					skeleton += '	<div class="vbo-dashboard-guest-activity-content">';
					skeleton += '		<div class="vbo-dashboard-guest-activity-content-head">';
					skeleton += '			<div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
					skeleton += '		</div>';
					skeleton += '		<div class="vbo-dashboard-guest-activity-content-subhead">';
					skeleton += '			<div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
					skeleton += '		</div>';
					skeleton += '		<div class="vbo-dashboard-guest-activity-content-info-msg">';
					skeleton += '			<div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
					skeleton += '		</div>';
					skeleton += '	</div>';
					skeleton += '</div>';
					// append skeleton
					jQuery(skeleton).appendTo(widget_instance.find('.vbo-dashboard-guest-activities-list'));
				}
			}

			/**
			 * Perform the request to load the latest activities.
			 */
			function vboWidgetLatestFromGuestsLoad(wrapper) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}
				var current_offset  = parseInt(widget_instance.attr('data-offset'));
				var length_per_page = parseInt(widget_instance.attr('data-length'));

				// the widget method to call
				var call_method = 'loadActivities';

				// make a request to load the activities
				VBOCore.doAjax(
					"<?php echo $this->getExecWidgetAjaxUri(); ?>",
					{
						widget_id: "<?php echo $this->getIdentifier(); ?>",
						call: call_method,
						offset: current_offset,
						length: length_per_page,
						wrapper: wrapper,
						tmpl: "component"
					},
					(response) => {
						try {
							var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
							if (!obj_res.hasOwnProperty(call_method)) {
								console.error('Unexpected JSON response', obj_res);
								return false;
							}
							// response must contain 4 values separated by ";call_method;"
							var activities_data = obj_res[call_method].split(';' + call_method + ';');
							if (activities_data.length != 4) {
								return;
							}
							// replace HTML with new activities
							widget_instance.find('.vbo-dashboard-guest-activities-list').html(activities_data[0]);
							// check if latest datetime is set
							if (activities_data[3].length) {
								widget_instance.attr('data-latestdt', activities_data[3]);
							}
							// check results
							if (!isNaN(activities_data[1]) && parseInt(activities_data[1]) < 1) {
								// no results can indicate the offset is invalid or too high
								if (!isNaN(activities_data[2]) && parseInt(activities_data[2]) > 0) {
									// reset offset to 0
									widget_instance.attr('data-offset', 0);
									// show loading skeletons
									vboWidgetLatestFromGuestsSkeletons(wrapper);
									// reload the first page
									vboWidgetLatestFromGuestsLoad(wrapper);
								}
							}
						} catch(err) {
							console.error('could not parse JSON response', err, response);
						}
					},
					(error) => {
						// remove the skeleton loading
						widget_instance.find('.vbo-dashboard-guest-activities-list').find('.vbo-dashboard-guest-activity-skeleton').remove();
						console.error(error);
					}
				);
			}

			/**
			 * Navigate between the various pages of the activities.
			 */
			function vboWidgetLatestFromGuestsNavigate(wrapper, direction) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				// current offset
				var current_offset = parseInt(widget_instance.attr('data-offset'));

				// steps per type
				var steps = <?php echo $this->activities_per_type; ?>;

				// show loading skeletons
				vboWidgetLatestFromGuestsSkeletons(wrapper);

				// check direction and update offset for next nav
				if (direction > 0) {
					// navigate forward
					widget_instance.attr('data-offset', (current_offset + steps));
				} else {
					// navigate backward
					var new_offset = current_offset - steps;
					new_offset = new_offset >= 0 ? new_offset : 0;
					widget_instance.attr('data-offset', new_offset);
				}
				
				// launch navigation
				vboWidgetLatestFromGuestsLoad(wrapper);
			}

			/**
			 * Watch periodically if there are new activities to be displayed.
			 */
			function vboWidgetLatestFromGuestsWatch(wrapper) {
				var widget_instance = jQuery('#' + wrapper);
				if (!widget_instance.length) {
					return false;
				}

				var latest_dt = widget_instance.attr('data-latestdt');
				if (!latest_dt || !latest_dt.length) {
					return false;
				}

				// the widget method to call
				var call_method = 'watchActivities';

				// make a request to watch the activities
				VBOCore.doAjax(
					"<?php echo $this->getExecWidgetAjaxUri(); ?>",
					{
						widget_id: "<?php echo $this->getIdentifier(); ?>",
						call: call_method,
						latest_dt: latest_dt,
						tmpl: "component"
					},
					(response) => {
						try {
							var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
							if (!obj_res.hasOwnProperty(call_method)) {
								console.error('Unexpected JSON response', obj_res);
								return false;
							}
							// response will contain the number of new activities
							if (isNaN(obj_res[call_method]) || parseInt(obj_res[call_method]) < 1) {
								// do nothing
								return;
							}
							// new activities found, reset the offset and re-load the first page
							widget_instance.attr('data-offset', 0);
							// show loading skeletons
							vboWidgetLatestFromGuestsSkeletons(wrapper);
							// reload the first page
							vboWidgetLatestFromGuestsLoad(wrapper);
						} catch(err) {
							console.error('could not parse JSON response', err, response);
						}
					},
					(error) => {
						// do nothing
						console.error(error);
					}
				);
			}
			
		</script>
		<?php
		}
		?>

		<script type="text/javascript">

			jQuery(function() {

				// when document is ready, load latest activities for this widget's instance
				vboWidgetLatestFromGuestsLoad('<?php echo $wrapper_id; ?>');

				// set interval for loading new activities automatically
				var watch_intv = setInterval(function() {
					vboWidgetLatestFromGuestsWatch('<?php echo $wrapper_id; ?>');
				}, 60000);

			<?php
			if ($js_intvals_id) {
				// widget can be dismissed through the modal
				?>
				document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_intvals_id; ?>', (e) => {
					clearInterval(watch_intv);
				});
				<?php
			}
			?>

			});

		</script>

		<?php
	}
}
