/** * User Alert System * Monitors shared notes and alerts when someone shares a note with the current user * * @module UserAlertSystem * @description Standalone utility for monitoring PocketBase note collection changes. * Watches for notes shared with current user and triggers desktop notifications and * audio alerts. Exports UserAlertSystem class for instantiation with PocketBase instance. * @usage Instantiate with PocketBase instance: new UserAlertSystem(pb, userEmail, userName). * Call watchForSharedNotes() to start monitoring. * @dependencies PocketBase instance (passed in constructor). Browser APIs: Notifications, * Web Audio API, DOM manipulation. * @integration Used in index.html to provide real-time alert functionality for shared notes * @gotchas Requires user permission for desktop notifications. Audio context may be blocked * on some browsers until user gesture. Track alertedNotes Set to prevent duplicate alerts. */ export class UserAlertSystem { constructor(pocketbaseInstance, userEmail, userName) { this.pb = pocketbaseInstance; this.userEmail = userEmail; this.userName = userName; this.alertedNotes = new Set(); // Track already-alerted notes to avoid duplicates this.unsubscribe = null; } /** * Play notification sound - double bell/chime */ async playNotificationSound() { try { // Create a simple double bell tone using Web Audio API const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const now = audioContext.currentTime; // First bell this.playBell(audioContext, now, 0); // Second bell (offset by 200ms) this.playBell(audioContext, now + 0.2, 0.7); } catch (error) { console.error('Failed to play notification sound:', error); // Fallback: try to play a system notification beep try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.connect(gain); gain.connect(audioContext.destination); osc.frequency.value = 800; gain.gain.setValueAtTime(0.3, audioContext.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); osc.start(audioContext.currentTime); osc.stop(audioContext.currentTime + 0.1); } catch (e) { console.error('Fallback notification sound also failed:', e); } } } /** * Play a single bell tone */ playBell(audioContext, startTime, volume = 1) { const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.connect(gain); gain.connect(audioContext.destination); // Bell frequency (higher pitch for notification) osc.frequency.setValueAtTime(1000, startTime); osc.frequency.exponentialRampToValueAtTime(600, startTime + 0.15); // Bell envelope (fade out) gain.gain.setValueAtTime(0.3 * volume, startTime); gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.2); osc.start(startTime); osc.stop(startTime + 0.2); } /** * Check if a name matches the current user * Searches Associations collection for first_name match and verifies email */ async verifyUserMatch(firstName) { try { console.log(`Checking for user match: firstName="${firstName}", currentEmail="${this.userEmail}"`); // Search Associations collection for matching first_name const records = await this.pb.collection('Associations').getList(1, 50, { filter: `first_name = "${firstName}"`, }); console.log(`Found ${records.items.length} records with first_name="${firstName}"`); if (records.items.length === 0) { console.log(`No user found with first name: ${firstName}`); return false; } // Check if any matching record has the current user's email const match = records.items.find((record) => { console.log(`Checking record: ${record.first_name} ${record.last_name} (${record.email})`); return record.email === this.userEmail; }); if (match) { console.log(`✓ User match confirmed: ${firstName} (${this.userEmail})`); return true; } console.log(`No email match for ${firstName} and ${this.userEmail}`); return false; } catch (error) { console.error('Error verifying user match:', error); return false; } } /** * Initialize real-time monitoring of Notes collection */ async startMonitoring() { try { console.log('🔔 Starting user alert monitoring...'); // Subscribe to Notes collection changes this.unsubscribe = await this.pb.collection('Notes').subscribe('*', async (event) => { try { const note = event.record; console.log(`📝 Note event (${event.action}):`, note.title, `shared=${note.shared}`, `shared_with=`, note.shared_with); // Only process shared notes if (!note.shared) { console.log(' → Skipped: not shared'); return; } // Check if current user is in shared_with list const sharedWithNames = Array.isArray(note.shared_with) ? note.shared_with : typeof note.shared_with === 'string' ? note.shared_with.split(',').map((n) => n.trim()) : []; console.log(` → shared_with names:`, sharedWithNames); // Extract first names from shared_with and check for match let userIsRecipient = false; for (const sharedName of sharedWithNames) { const firstName = sharedName.split(' ')[0]; console.log(` → Checking if ${firstName} matches...`); const isMatch = await this.verifyUserMatch(firstName); if (isMatch) { userIsRecipient = true; break; } } if (!userIsRecipient) { console.log(' → Skipped: user not in shared_with'); return; } // Check if we've already alerted for this note if (this.alertedNotes.has(note.id)) { console.log(' → Skipped: already alerted for this note'); return; } // Mark as alerted and play sound this.alertedNotes.add(note.id); console.log(`🔔 Alert! Note "${note.title}" shared with you`); await this.playNotificationSound(); } catch (error) { console.error('Error processing note event:', error); } }); console.log('✓ User alert monitoring started'); } catch (error) { console.error('Failed to start monitoring:', error); } } /** * Stop monitoring */ stopMonitoring() { if (this.unsubscribe) { this.unsubscribe(); this.unsubscribe = null; console.log('⏹ User alert monitoring stopped'); } } } /** * Initialize user alert system * Call this after user authentication */ export async function initializeUserAlert(pb, userEmail, userName) { const alertSystem = new UserAlertSystem(pb, userEmail, userName); await alertSystem.startMonitoring(); return alertSystem; }