195 lines
5.5 KiB
JavaScript
195 lines
5.5 KiB
JavaScript
// 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; |