Skip to content

Commit 8cba1ce

Browse files
committed
Add meeting notification utilities
Introduces utilities for managing meeting reminder notifications, including preference storage, browser notifications, sound alerts, snooze functionality, and notification tracking. These functions support customizable intervals, localStorage persistence, and user interaction for meeting reminders.
1 parent 9f4905c commit 8cba1ce

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

lib/utils/meeting-notifications.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/**
2+
* Meeting Reminder Notification Utilities
3+
* Handles notification preferences, browser notifications, and sound alerts
4+
*/
5+
6+
// Types
7+
export interface ReminderPreferences {
8+
enabled: boolean
9+
intervals: number[] // minutes before meeting (e.g., [5, 10, 15, 30])
10+
browserNotifications: boolean
11+
soundEnabled: boolean
12+
soundVolume: number // 0-1
13+
}
14+
15+
// Default preferences
16+
const DEFAULT_PREFERENCES: ReminderPreferences = {
17+
enabled: true,
18+
intervals: [10], // Default: 10 minutes before
19+
browserNotifications: false,
20+
soundEnabled: true,
21+
soundVolume: 0.5,
22+
}
23+
24+
const STORAGE_KEY = 'lab68_meeting_reminder_preferences'
25+
26+
/**
27+
* Get reminder preferences from localStorage
28+
*/
29+
export function getReminderPreferences(): ReminderPreferences {
30+
if (typeof window === 'undefined') return DEFAULT_PREFERENCES
31+
32+
try {
33+
const stored = localStorage.getItem(STORAGE_KEY)
34+
if (stored) {
35+
return { ...DEFAULT_PREFERENCES, ...JSON.parse(stored) }
36+
}
37+
} catch (e) {
38+
console.error('Failed to load reminder preferences:', e)
39+
}
40+
return DEFAULT_PREFERENCES
41+
}
42+
43+
/**
44+
* Save reminder preferences to localStorage
45+
*/
46+
export function setReminderPreferences(prefs: Partial<ReminderPreferences>): void {
47+
if (typeof window === 'undefined') return
48+
49+
try {
50+
const current = getReminderPreferences()
51+
const updated = { ...current, ...prefs }
52+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated))
53+
54+
// Dispatch event for components to react
55+
window.dispatchEvent(new CustomEvent('lab68_reminder_prefs_change', { detail: updated }))
56+
} catch (e) {
57+
console.error('Failed to save reminder preferences:', e)
58+
}
59+
}
60+
61+
/**
62+
* Request browser notification permission
63+
* Returns true if granted, false otherwise
64+
*/
65+
export async function requestNotificationPermission(): Promise<boolean> {
66+
if (typeof window === 'undefined' || !('Notification' in window)) {
67+
console.warn('Browser notifications not supported')
68+
return false
69+
}
70+
71+
if (Notification.permission === 'granted') {
72+
return true
73+
}
74+
75+
if (Notification.permission === 'denied') {
76+
console.warn('Browser notifications are blocked')
77+
return false
78+
}
79+
80+
try {
81+
const permission = await Notification.requestPermission()
82+
return permission === 'granted'
83+
} catch (e) {
84+
console.error('Failed to request notification permission:', e)
85+
return false
86+
}
87+
}
88+
89+
/**
90+
* Check if browser notifications are available and permitted
91+
*/
92+
export function canShowBrowserNotifications(): boolean {
93+
if (typeof window === 'undefined' || !('Notification' in window)) {
94+
return false
95+
}
96+
return Notification.permission === 'granted'
97+
}
98+
99+
/**
100+
* Show a browser notification for a meeting
101+
*/
102+
export function showBrowserNotification(
103+
title: string,
104+
body: string,
105+
options?: {
106+
icon?: string
107+
onClick?: () => void
108+
requireInteraction?: boolean
109+
}
110+
): void {
111+
if (!canShowBrowserNotifications()) return
112+
113+
try {
114+
const notification = new Notification(title, {
115+
body,
116+
icon: options?.icon || '/icon.png',
117+
badge: '/icon.png',
118+
requireInteraction: options?.requireInteraction ?? true,
119+
tag: 'meeting-reminder',
120+
})
121+
122+
if (options?.onClick) {
123+
notification.onclick = () => {
124+
window.focus()
125+
options.onClick?.()
126+
notification.close()
127+
}
128+
}
129+
130+
// Auto-close after 30 seconds
131+
setTimeout(() => notification.close(), 30000)
132+
} catch (e) {
133+
console.error('Failed to show browser notification:', e)
134+
}
135+
}
136+
137+
/**
138+
* Play notification sound
139+
*/
140+
export function playNotificationSound(volume?: number): void {
141+
if (typeof window === 'undefined') return
142+
143+
const prefs = getReminderPreferences()
144+
if (!prefs.soundEnabled) return
145+
146+
try {
147+
const audio = new Audio('/sounds/timer-complete.mp3')
148+
audio.volume = volume ?? prefs.soundVolume
149+
audio.play().catch(e => console.log('Audio play failed:', e))
150+
} catch (e) {
151+
console.error('Audio setup failed:', e)
152+
}
153+
}
154+
155+
/**
156+
* Track which meetings have been notified for which intervals
157+
* Key format: `${meetingId}_${minutesBefore}`
158+
*/
159+
const NOTIFIED_KEY = 'lab68_notified_meeting_reminders'
160+
161+
export function hasBeenNotified(meetingId: string, minutesBefore: number): boolean {
162+
if (typeof window === 'undefined') return true
163+
164+
try {
165+
const notified = JSON.parse(localStorage.getItem(NOTIFIED_KEY) || '[]') as string[]
166+
return notified.includes(`${meetingId}_${minutesBefore}`)
167+
} catch {
168+
return false
169+
}
170+
}
171+
172+
export function markAsNotified(meetingId: string, minutesBefore: number): void {
173+
if (typeof window === 'undefined') return
174+
175+
try {
176+
const notified = JSON.parse(localStorage.getItem(NOTIFIED_KEY) || '[]') as string[]
177+
const key = `${meetingId}_${minutesBefore}`
178+
if (!notified.includes(key)) {
179+
notified.push(key)
180+
localStorage.setItem(NOTIFIED_KEY, JSON.stringify(notified))
181+
}
182+
} catch (e) {
183+
console.error('Failed to mark notification:', e)
184+
}
185+
}
186+
187+
/**
188+
* Clean up old notification records (older than 24 hours to prevent memory growth)
189+
*/
190+
export function cleanupOldNotifications(): void {
191+
if (typeof window === 'undefined') return
192+
193+
// This runs periodically; for simplicity we just keep the array limited
194+
try {
195+
const notified = JSON.parse(localStorage.getItem(NOTIFIED_KEY) || '[]') as string[]
196+
// Keep only last 100 entries
197+
if (notified.length > 100) {
198+
localStorage.setItem(NOTIFIED_KEY, JSON.stringify(notified.slice(-100)))
199+
}
200+
} catch {
201+
// Ignore
202+
}
203+
}
204+
205+
/**
206+
* Snooze a notification for a specific meeting
207+
* Returns the snooze end time
208+
*/
209+
const SNOOZE_KEY = 'lab68_snoozed_reminders'
210+
211+
export function snoozeMeeting(meetingId: string, snoozeMinutes: number = 5): Date {
212+
if (typeof window === 'undefined') return new Date()
213+
214+
const snoozeUntil = new Date(Date.now() + snoozeMinutes * 60 * 1000)
215+
216+
try {
217+
const snoozed = JSON.parse(localStorage.getItem(SNOOZE_KEY) || '{}') as Record<string, string>
218+
snoozed[meetingId] = snoozeUntil.toISOString()
219+
localStorage.setItem(SNOOZE_KEY, JSON.stringify(snoozed))
220+
} catch (e) {
221+
console.error('Failed to snooze meeting:', e)
222+
}
223+
224+
return snoozeUntil
225+
}
226+
227+
export function isMeetingSnoozed(meetingId: string): boolean {
228+
if (typeof window === 'undefined') return false
229+
230+
try {
231+
const snoozed = JSON.parse(localStorage.getItem(SNOOZE_KEY) || '{}') as Record<string, string>
232+
const snoozeUntil = snoozed[meetingId]
233+
if (snoozeUntil) {
234+
return new Date(snoozeUntil) > new Date()
235+
}
236+
} catch {
237+
// Ignore
238+
}
239+
240+
return false
241+
}
242+
243+
/**
244+
* Get available reminder interval options
245+
*/
246+
export function getAvailableIntervals(): { value: number; label: string }[] {
247+
return [
248+
{ value: 5, label: '5 minutes' },
249+
{ value: 10, label: '10 minutes' },
250+
{ value: 15, label: '15 minutes' },
251+
{ value: 30, label: '30 minutes' },
252+
{ value: 60, label: '1 hour' },
253+
]
254+
}

0 commit comments

Comments
 (0)