212 lines
5.4 KiB
JavaScript
212 lines
5.4 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
|
|
}
|
|
// Decide whether this device's user should be notified about a chore:
|
|
// - assignedTo set -> only that user
|
|
// - no assignedTo -> everyone listed in assignees
|
|
// - no assignees -> "Anyone" mode, notify the whole circle
|
|
const shouldNotifyUser = (chore, userId) => {
|
|
if (!userId) {
|
|
return false
|
|
}
|
|
if (chore.assignedTo > 0) {
|
|
return chore.assignedTo === userId
|
|
}
|
|
if (chore.assignees?.length > 0) {
|
|
return chore.assignees.some(assignee => assignee.userId === userId)
|
|
}
|
|
return true
|
|
}
|
|
|
|
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,
|
|
dueDate,
|
|
time,
|
|
)
|
|
if (time > now) {
|
|
notifications.push({
|
|
title,
|
|
body,
|
|
id: notificationId,
|
|
allowWhileIdle: true,
|
|
schedule: {
|
|
at: time,
|
|
},
|
|
extra: {
|
|
choreId: chore.id,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const getNotificationText = (
|
|
choreName,
|
|
template = {},
|
|
dueDate,
|
|
notificationTime,
|
|
) => {
|
|
const startOfDay = date =>
|
|
new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
const dayDifference = Math.round(
|
|
(startOfDay(dueDate) - startOfDay(notificationTime)) /
|
|
(24 * 60 * 60 * 1000),
|
|
)
|
|
const time = dueDate.toLocaleTimeString([], {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
})
|
|
|
|
let dueTime
|
|
if (dayDifference === 0) {
|
|
dueTime = `today at ${time}`
|
|
} else if (dayDifference === 1) {
|
|
dueTime = `tomorrow at ${time}`
|
|
} else if (dayDifference === -1) {
|
|
dueTime = `yesterday at ${time}`
|
|
} else {
|
|
const date = dueDate.toLocaleDateString([], {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})
|
|
dueTime = `${date} at ${time}`
|
|
}
|
|
|
|
let body
|
|
if (template.value < 0) {
|
|
body = `Due ${dueTime}`
|
|
} else if (template.value > 0) {
|
|
body = `Overdue · Was due ${dueTime}`
|
|
} else {
|
|
body = 'Due now'
|
|
}
|
|
|
|
return {
|
|
title: choreName,
|
|
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 ||
|
|
chore.isActive === false ||
|
|
!shouldNotifyUser(chore, userProfile?.id)
|
|
) {
|
|
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 }
|