Initial commit
This commit is contained in:
195
resources/js/presence-tracker.js
Normal file
195
resources/js/presence-tracker.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user