208 lines
7.1 KiB
JavaScript
208 lines
7.1 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|