Files
donetick/src/views/Chores/LocalNotificationScheduler.js

226 lines
6.3 KiB
JavaScript

import { Capacitor } from '@capacitor/core'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Preferences } from '@capacitor/preferences'
import murmurhash from 'murmurhash'
const MAX_LOCAL_NOTIFICATIONS = 64
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' })
return JSON.parse(ret.value)
}
const canScheduleNotification = async () => {
if (Capacitor.isNativePlatform() === false) {
return false
}
const notificationPreferences = await getNotificationPreferences()
console.log('Notification preferences:', notificationPreferences)
if (notificationPreferences['granted'] === false) {
return false
}
return true
}
const getIdFromTemplate = (choreId, template) => {
const hash = murmurhash.v3(`${choreId}-${template.value}-${template.unit}`)
// Use Math.abs() with modulo to ensure positive ID within Java int range
// This guarantees the ID is always positive and within 1 to 2^31-1
return Math.abs(hash) % 2147483647
}
const getTimeFromTemplate = (template, relativeTime) => {
let time = relativeTime
switch (template.unit) {
case 'm':
time = new Date(relativeTime.getTime() + template.value * 60 * 1000)
break
case 'h':
time = new Date(relativeTime.getTime() + template.value * 60 * 60 * 1000)
break
case 'd':
time = new Date(
relativeTime.getTime() + template.value * 24 * 60 * 60 * 1000,
)
break
default:
time = relativeTime
}
return time
}
const scheduleNotificationFromTemplate = (
chore,
userProfile,
allPerformers,
notifications,
) => {
for (const template of chore.notificationMetadata?.templates || []) {
// convert the template to time:
const dueDate = new Date(chore.nextDueDate)
const now = new Date()
const time = getTimeFromTemplate(template, dueDate)
const notificationId = getIdFromTemplate(chore.id, template)
const { title, body } = getNotificationText(chore.name, template)
if (time > now) {
notifications.push({
title,
body: `${body} at ${time.toLocaleTimeString()}`,
id: notificationId,
allowWhileIdle: true,
schedule: {
at: time,
},
extra: {
choreId: chore.id,
},
})
}
}
}
const getNotificationText = (choreName, template = {}) => {
// Determine notification type based on template value
const getNotificationType = () => {
if (!template || template.value === undefined) {
return 'due'
}
if (template.value < 0) {
return 'reminder'
} else if (template.value === 0) {
return 'due'
} else {
return 'overdue'
}
}
const notificationType = getNotificationType()
// Truncate chore name if too long for better readability
const maxChoreNameLength = 25
const truncatedName =
choreName.length > maxChoreNameLength
? `${choreName.substring(0, maxChoreNameLength)}...`
: choreName
// Generate time-based descriptive text
const getTimeDescription = () => {
if (!template || !template.value || !template.unit) {
return 'soon'
}
const { value, unit } = template
const absValue = Math.abs(value)
switch (unit) {
case 'm':
if (absValue === 1) return value < 0 ? 'in 1 minute' : '1 minute ago'
if (absValue < 60)
return value < 0
? `in ${absValue} minutes`
: `${absValue} minutes ago`
break
case 'h':
if (absValue === 1) return value < 0 ? 'in 1 hour' : '1 hour ago'
if (absValue < 24)
return value < 0 ? `in ${absValue} hours` : `${absValue} hours ago`
break
case 'd':
if (absValue === 1) return value < 0 ? 'tomorrow' : 'yesterday'
if (absValue === 7) return value < 0 ? 'next week' : 'last week'
if (absValue < 7)
return value < 0 ? `in ${absValue} days` : `${absValue} days ago`
if (absValue < 30) {
const weeks = Math.round(absValue / 7)
return value < 0 ? `in ${weeks} weeks` : `${weeks} weeks ago`
}
break
default:
return value < 0 ? `in ${absValue} ${unit}` : `${absValue} ${unit} ago`
}
return value < 0 ? `in ${absValue} ${unit}` : `${absValue} ${unit} ago`
}
const messages = {
reminder: {
title: `📋 ${truncatedName}`,
body: `Reminder: Due ${getTimeDescription()}`,
},
due: {
title: `🔔 ${truncatedName}`,
body: 'Due now - Time to get started!',
},
overdue: {
title: `${truncatedName}`,
body: `Overdue ${getTimeDescription()} - Complete when you can`,
},
}
// Fallback to due if type not found
const messageTemplate = messages[notificationType] || messages.due
return {
title: messageTemplate.title,
body: messageTemplate.body,
}
}
const cancelPendingNotifications = async () => {
try {
const pending = await LocalNotifications.getPending()
if (pending.notifications.length > 0) {
await LocalNotifications.cancel({ notifications: pending.notifications })
console.log('Cancelled pending notifications:', pending.notifications)
} else {
console.log('No pending notifications to cancel.')
}
} catch (error) {
console.error('Error cancelling pending notifications:', error)
}
}
const scheduleChoreNotification = async (
chores,
userProfile,
allPerformers,
) => {
await cancelPendingNotifications()
const notifications = []
for (let i = 0; i < chores.length; i++) {
const chore = chores[i]
try {
if (chore.notification === false || chore.nextDueDate === null) {
continue
}
scheduleNotificationFromTemplate(
chore,
userProfile,
allPerformers,
notifications,
)
} catch (error) {
console.error(
'Error parsing notification metadata for chore:',
chore.id,
error,
)
continue
}
}
// sort from soonest to latest:
notifications.sort((a, b) => a.schedule.at - b.schedule.at)
// cap it for 64 notifications for Android:
if (notifications.length > MAX_LOCAL_NOTIFICATIONS) {
notifications.splice(MAX_LOCAL_NOTIFICATIONS)
}
LocalNotifications.schedule({
notifications,
})
return notifications
}
export { canScheduleNotification, scheduleChoreNotification }