// Enhanced presence tracker with AJAX fallback class PresenceTracker { constructor() { this.heartbeatInterval = null; this.offlineTimeout = null; this.lastActivity = Date.now(); this.isOnline = true; this.init(); } init() { this.setupActivityTracking(); this.setupVisibilityHandling(); this.setupBeforeUnload(); this.startHeartbeat(); } setupActivityTracking() { const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; events.forEach(event => { document.addEventListener(event, () => { this.updateActivity(); }, { passive: true }); }); } setupVisibilityHandling() { document.addEventListener('visibilitychange', () => { if (document.hidden) { this.handleUserAway(); } else { this.handleUserBack(); } }); } setupBeforeUnload() { window.addEventListener('beforeunload', () => { this.setOffline(); }); window.addEventListener('pagehide', () => { this.setOffline(); }); } updateActivity() { const now = Date.now(); if (now - this.lastActivity > 15000) { // 15 seconds this.lastActivity = now; if (this.offlineTimeout) { clearTimeout(this.offlineTimeout); } // Set offline timeout for 5 minutes of inactivity this.offlineTimeout = setTimeout(() => { this.setOffline(); }, 300000); // Update Livewire components this.notifyLivewireComponents('handleUserActivity'); } } handleUserAway() { // Set shorter offline timeout when tab is hidden if (this.offlineTimeout) { clearTimeout(this.offlineTimeout); } this.offlineTimeout = setTimeout(() => { this.setOffline(); }, 30000); // 30 seconds when tab is hidden } handleUserBack() { this.lastActivity = Date.now(); this.heartbeat(); // Immediate heartbeat when coming back // Reset normal offline timeout if (this.offlineTimeout) { clearTimeout(this.offlineTimeout); } this.offlineTimeout = setTimeout(() => { this.setOffline(); }, 300000); // 5 minutes } startHeartbeat() { // Send heartbeat every 30 seconds this.heartbeatInterval = setInterval(() => { if (!document.hidden && this.isOnline) { this.heartbeat(); } }, 30000); } heartbeat() { const guards = this.getActiveGuards(); guards.forEach(guard => { fetch('/presence/heartbeat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content }, body: JSON.stringify({ guard: guard }) }).then(response => { if (response.ok) { this.isOnline = true; this.notifyLivewireComponents('handleUserActivity'); } }).catch(() => {}); }); } setOffline() { if (!this.isOnline) return; // Already offline const guards = this.getActiveGuards(); guards.forEach(guard => { // Use sendBeacon for reliability during page unload const data = JSON.stringify({ guard: guard }); if (navigator.sendBeacon) { navigator.sendBeacon('/presence/offline', data); } else { fetch('/presence/offline', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content }, body: data }).catch(() => {}); } }); this.isOnline = false; this.notifyLivewireComponents('handleUserOffline'); } getActiveGuards() { const guards = new Set(); document.querySelectorAll('.user-presence-container').forEach(container => { const guard = container.getAttribute('data-guard') || 'web'; guards.add(guard); }); return Array.from(guards); } notifyLivewireComponents(method) { document.querySelectorAll('[wire\\:id]').forEach(component => { const componentId = component.getAttribute('wire:id'); if (componentId && component.classList.contains('user-presence-container')) { try { Livewire.find(componentId).call(method); } catch (e) { // component not available } } }); } destroy() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } if (this.offlineTimeout) { clearTimeout(this.offlineTimeout); } this.setOffline(); } } // Initialize presence tracker document.addEventListener('DOMContentLoaded', () => { window.presenceTracker = new PresenceTracker(); }); // Cleanup on page unload window.addEventListener('beforeunload', () => { if (window.presenceTracker) { window.presenceTracker.destroy(); } }); export default PresenceTracker;