diff --git a/index.html b/index.html index f49e418..95e393c 100644 --- a/index.html +++ b/index.html @@ -333,6 +333,35 @@ console.log('✓ Capture tools initialized successfully'); console.log('Available: window.captureScreenshot, window.captureSnip, window.canvasToFile, window.recordScreen'); + + // Alert sound function + window.testAlertSound = async function() { + try { + console.log('🔔 Testing alert sound...'); + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const now = audioContext.currentTime; + + const playBell = (startTime, volume = 1) => { + const osc = audioContext.createOscillator(); + const gain = audioContext.createGain(); + osc.connect(gain); + gain.connect(audioContext.destination); + osc.frequency.setValueAtTime(1000, startTime); + osc.frequency.exponentialRampToValueAtTime(600, startTime + 0.15); + gain.gain.setValueAtTime(0.3 * volume, startTime); + gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.2); + osc.start(startTime); + osc.stop(startTime + 0.2); + }; + + playBell(now, 1); + playBell(now + 0.2, 0.7); + console.log('✓ Sound played'); + } catch (error) { + console.error('Failed to play sound:', error); + } + }; + console.log('✓ testAlertSound available - call window.testAlertSound()');
@@ -1358,6 +1387,76 @@ console.log(`User display name: ${displayName}`); console.log(`User email: ${email}`); console.log('PB token:', pb.authStore.token ? '(present)' : '(missing)'); + + // Initialize user alert system for shared notes + try { + const alertedNotes = new Set(); + + async function verifyUserMatch(firstName) { + try { + console.log(` Checking: firstName="${firstName}", email="${email}"`); + const records = await pb.collection('Associations').getList(1, 50, { + filter: `first_name = "${firstName}"`, + }); + + console.log(` Found ${records.items.length} records`); + + const match = records.items.find((record) => { + console.log(` Checking: ${record.first_name} ${record.last_name} (${record.email})`); + return record.email === email; + }); + + return !!match; + } catch (error) { + console.error('Error verifying user match:', error); + return false; + } + } + + console.log('🔔 Starting user alert monitoring...'); + await pb.collection('Notes').subscribe('*', async (event) => { + try { + const note = event.record; + console.log(`📝 Note event (${event.action}):`, note.title, `shared=${note.shared}`); + + if (!note.shared) { + return; + } + + 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:`, sharedWithNames); + + let userIsRecipient = false; + for (const sharedName of sharedWithNames) { + const firstName = sharedName.split(' ')[0]; + if (await verifyUserMatch(firstName)) { + userIsRecipient = true; + break; + } + } + + if (!userIsRecipient || alertedNotes.has(note.id)) { + return; + } + + alertedNotes.add(note.id); + console.log(`🔔🔔 ALERT! Note "${note.title}" shared with you`); + await window.testAlertSound(); + } catch (error) { + console.error('Error processing note event:', error); + } + }); + + console.log('✓ User alert monitoring started'); + } catch (error) { + console.error('Failed to initialize user alert system:', error); + } + updateAuthUI(); await checkTokens(); } catch (error) { diff --git a/tools/user-alert/index.js b/tools/user-alert/index.js new file mode 100644 index 0000000..d67dc1a --- /dev/null +++ b/tools/user-alert/index.js @@ -0,0 +1,195 @@ +/** + * User Alert System + * Monitors shared notes and alerts when someone shares a note with the current user + */ + +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; +}