From 4abe86d00b363ade15a87600a268743100456453 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 31 Oct 2023 14:09:12 -0600 Subject: [PATCH 01/36] Add notifScheduler.ts Translated notifScheduler.js from Angular to React Typescript commHelper.ts - Added a temporary "any" return type for getUser because the TS needed it ProfileSettings.jsx - Imported the new useSchedulerHelper function (name in progress - need to find a good name for this hook) - Replaced any instances of the old Angular NotificationScheduler module with the new schedulerHelper hook notifScheduler.ts - Translated the file from Angular to React TS - This file exports a single function which initializes the appConfig and then returns an object containing the functions that the Notification Scheduler functions need (only ProfileSettings.jsx is using this file currently) - Replaced any instances of moment with Luxon equivalent --- www/js/commHelper.ts | 2 +- www/js/control/ProfileSettings.jsx | 8 +- www/js/splash/notifScheduler.ts | 286 +++++++++++++++++++++++++++++ 3 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 www/js/splash/notifScheduler.ts diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index b9584a044..1e42633cc 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -147,7 +147,7 @@ export function updateUser(updateDoc) { }); } -export function getUser() { +export function getUser(): any { return new Promise((rs, rj) => { window['cordova'].plugins.BEMServerComm.getUserPersonalData("/profile/get", rs, rj); }).catch(error => { diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 7678cae6b..5b2cac94c 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -25,6 +25,7 @@ import { AppContext } from "../App"; import { shareQR } from "../components/QrCode"; import { storageClear } from "../plugin/storage"; import { getAppVersion } from "../plugin/clientStats"; +import { useSchedulerHelper } from "../splash/notifScheduler"; //any pure functions can go outside const ProfileSettings = () => { @@ -33,6 +34,7 @@ const ProfileSettings = () => { const appConfig = useAppConfig(); const { colors } = useTheme(); const { setPermissionsPopupVis } = useContext(AppContext); + const schedulerHelper = useSchedulerHelper(); //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); @@ -173,13 +175,13 @@ const ProfileSettings = () => { const newNotificationSettings ={}; if (uiConfig?.reminderSchemes) { - const prefs = await NotificationScheduler.getReminderPrefs(); + const prefs = await schedulerHelper.getReminderPrefs(); const m = moment(prefs.reminder_time_of_day, 'HH:mm'); newNotificationSettings.prefReminderTimeVal = m.toDate(); const n = moment(newNotificationSettings.prefReminderTimeVal); newNotificationSettings.prefReminderTime = n.format('LT'); newNotificationSettings.prefReminderTimeOnLoad = prefs.reminder_time_of_day; - newNotificationSettings.scheduledNotifs = await NotificationScheduler.getScheduledNotifs(); + newNotificationSettings.scheduledNotifs = await schedulerHelper.getScheduledNotifs(); updatePrefReminderTime(false); } @@ -243,7 +245,7 @@ const ProfileSettings = () => { if(storeNewVal){ const m = moment(newTime); // store in HH:mm - NotificationScheduler.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }).then(() => { + schedulerHelper.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }).then(() => { refreshNotificationSettings(); }); } diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts new file mode 100644 index 000000000..0474fca5d --- /dev/null +++ b/www/js/splash/notifScheduler.ts @@ -0,0 +1,286 @@ +import angular from 'angular'; +import React, { useEffect, useState } from "react"; +import { getConfig } from '../config/dynamicConfig'; +import useAppConfig from "../useAppConfig"; +import { addStatReading, statKeys } from '../plugin/clientStats'; +import { getUser, updateUser } from '../commHelper'; +import { logDebug } from "../plugin/logger"; +import { DateTime } from "luxon"; +import i18next from 'i18next'; + +let scheduledPromise = new Promise((rs) => rs()); +let scheduledNotifs = []; +let isScheduling = false; + +// like python range() +function range(start, stop, step) { + let a = [start], b = start; + while (b < stop) + a.push(b += step || 1); + return a; +} + +// returns an array of moment objects, for all times that notifications should be sent +const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { + const notifTimes = []; + for (const s of scheme.schedule) { + // the days to send notifications, as integers, relative to day zero + const notifDays = range(s.start, s.end, s.intervalInDays); + for (const d of notifDays) { + const date = DateTime.fromFormat(dayZeroDate, 'yyyy-MM-dd').plus({ days: d}).toFormat('yyyy-MM-dd') + const notifTime = DateTime.fromFormat(date+' '+timeOfDay, 'yyyy-MM-dd HH:mm'); + notifTimes.push(notifTime); + } + } + return notifTimes; +} + +// returns true if all expected times are already scheduled +const areAlreadyScheduled = (notifs, expectedTimes) => { + for (const t of expectedTimes) { + if (!notifs.some((n) => DateTime.fromMillis(n.trigger.at).equals(t))) { + return false; + } + } + return true; +} + +/* remove notif actions as they do not work, can restore post routing migration */ +// const setUpActions = () => { +// const action = { +// id: 'action', +// title: 'Change Time', +// launch: true +// }; +// return new Promise((rs) => { +// cordova.plugins.notification.local.addActions('reminder-actions', [action], rs); +// }); +// } +function debugGetScheduled(prefix) { + window['cordova'].plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length) + return logDebug(`${prefix}, there are no scheduled notifications`); + const time = DateTime.fromMillis(notifs?.[0].trigger.at).toFormat('HH:mm'); + //was in plugin, changed to scheduler + scheduledNotifs = notifs.map((n) => { + const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); + const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + return { + key: date, + val: time + } + }); + //have the list of scheduled show up in this log + logDebug(`${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduledNotifs[0].key} at ${scheduledNotifs[0].val}`); + }); +} + +//new method to fetch notifications +const getScheduledNotifs = function() { + return new Promise((resolve, reject) => { + /* if the notifications are still in active scheduling it causes problems + anywhere from 0-n of the scheduled notifs are displayed + if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors + */ + if(isScheduling) + { + console.log("requesting fetch while still actively scheduling, waiting on scheduledPromise"); + scheduledPromise.then(() => { + getNotifs().then((notifs) => { + console.log("done scheduling notifs", notifs); + resolve(notifs); + }) + }) + } + else{ + getNotifs().then((notifs) => { + resolve(notifs); + }) + } + }) +} + +//get scheduled notifications from cordova plugin and format them +const getNotifs = function() { + return new Promise((resolve, reject) => { + window['cordova'].plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length){ + console.log("there are no notifications"); + resolve([]); //if none, return empty array + } + + const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing + let scheduledNotifs = []; + scheduledNotifs = notifSubset.map((n) => { + const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); + const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + return { + key: date, + val: time + } + }); + resolve(scheduledNotifs); + }); + }) +} + +// schedules the notifications using the cordova plugin +const scheduleNotifs = (scheme, notifTimes) => { + return new Promise((rs) => { + isScheduling = true; + const localeCode = i18next.resolvedLanguage; + console.error("notifTimes: ", notifTimes, " - type: ", typeof(notifTimes)); + const nots = notifTimes.map((n) => { + console.error("n: ", n, " - type: ", typeof(n)); + const nDate = n.toDate(); + const seconds = nDate.getTime() / 1000; + return { + id: seconds, + title: scheme.title[localeCode], + text: scheme.text[localeCode], + trigger: {at: nDate}, + // actions: 'reminder-actions', + // data: { + // action: { + // redirectTo: 'root.main.control', + // redirectParams: { + // openTimeOfDayPicker: true + // } + // } + // } + } + }); + window['cordova'].plugins.notification.local.cancelAll(() => { + debugGetScheduled("After cancelling"); + window['cordova'].plugins.notification.local.schedule(nots, () => { + debugGetScheduled("After scheduling"); + isScheduling = false; + rs(); //scheduling promise resolved here + }); + }); + }); +} + +// determines when notifications are needed, and schedules them if not already scheduled +const update = async (reminderSchemes) => { + const { reminder_assignment, + reminder_join_date, + reminder_time_of_day} = await getReminderPrefs(reminderSchemes); + var scheme = {}; + try { + scheme = reminderSchemes[reminder_assignment]; + } catch (e) { + console.log("ERROR: Could not find reminder scheme for assignment " + reminderSchemes + " - " + reminder_assignment); + } + const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); + return new Promise((resolve, reject) => { + window['cordova'].plugins.notification.local.getScheduled((notifs) => { + if (areAlreadyScheduled(notifs, notifTimes)) { + logDebug("Already scheduled, not scheduling again"); + } else { + // to ensure we don't overlap with the last scheduling() request, + // we'll wait for the previous one to finish before scheduling again + scheduledPromise.then(() => { + if (isScheduling) { + console.log("ERROR: Already scheduling notifications, not scheduling again") + } else { + scheduledPromise = scheduleNotifs(scheme, notifTimes); + //enforcing end of scheduling to conisder update through + scheduledPromise.then(() => { + resolve(); + }) + } + }); + } + }); + }); +} + +/* Randomly assign a scheme, set the join date to today, + and use the default time of day from config (or noon if not specified) + This is only called once when the user first joins the study +*/ +const initReminderPrefs = (reminderSchemes) => { + // randomly assign from the schemes listed in config + const schemes = Object.keys(reminderSchemes); + const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; + const todayDate = DateTime.local().toFormat('yyyy-MM-dd'); + const defaultTime = reminderSchemes[randAssignment]?.defaultTime || '12:00'; + return { + reminder_assignment: randAssignment, + reminder_join_date: todayDate, + reminder_time_of_day: defaultTime, + }; +} + +/* EXAMPLE VALUES - present in user profile object + reminder_assignment: 'passive', + reminder_join_date: '2023-05-09', + reminder_time_of_day: '21:00', +*/ +// interface ReminderPrefs { +// reminder_assignment: string; +// reminder_join_date: string; +// reminder_time_of_day: string; +// } + +const getReminderPrefs = async (reminderSchemes): Promise => { + const user = await getUser(); + if (user?.reminder_assignment && + user?.reminder_join_date && + user?.reminder_time_of_day) { + console.log("User already has reminder prefs, returning them", user) + return user; + } + // if no prefs, user just joined, so initialize them + console.log("User just joined, Initializing reminder prefs") + const initPrefs = initReminderPrefs(reminderSchemes); + console.log("Initialized reminder prefs: ", initPrefs); + await setReminderPrefs(initPrefs, reminderSchemes); + return { ...user, ...initPrefs }; // user profile + the new prefs +} +const setReminderPrefs = async (newPrefs, reminderSchemes) => { + await updateUser(newPrefs) + const updatePromise = new Promise((resolve, reject) => { + //enforcing update before moving on + update(reminderSchemes).then(() => { + resolve(); + }); + }); + // record the new prefs in client stats + getReminderPrefs(reminderSchemes).then((prefs) => { + // extract only the relevant fields from the prefs, + // and add as a reading to client stats + const { reminder_assignment, + reminder_join_date, + reminder_time_of_day} = prefs; + addStatReading(statKeys.REMINDER_PREFS, { + reminder_assignment, + reminder_join_date, + reminder_time_of_day + }).then(logDebug("Added reminder prefs to client stats")); + }); + return updatePromise; +} + +export function useSchedulerHelper() { + const appConfig = useAppConfig(); + const [reminderSchemes, setReminderSchemes] = useState(); + + useEffect(() => { + if (!appConfig) { + logDebug("No reminder schemes found in config, not scheduling notifications"); + return; + } + setReminderSchemes(appConfig.reminderSchemes); + }, [appConfig]); + + //setUpActions(); + update(reminderSchemes); + + return { + setReminderPrefs: (newPrefs) => setReminderPrefs(newPrefs, reminderSchemes), + getReminderPrefs: () => getReminderPrefs(reminderSchemes), + getScheduledNotifs: () => getScheduledNotifs(), + } +} \ No newline at end of file From 131eca5f18c93e55b1904d4221fd1466a22352a8 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 2 Nov 2023 16:20:55 -0600 Subject: [PATCH 02/36] Format notifScheduler.ts Following the Prettier changes in @jiji14 commit bb73676c7245b68b9a8784204b68d71ba0af5267 --- www/js/splash/notifScheduler.ts | 418 ++++++++++++++++---------------- 1 file changed, 210 insertions(+), 208 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 0474fca5d..cd3385ff8 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -1,11 +1,11 @@ import angular from 'angular'; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; import { getConfig } from '../config/dynamicConfig'; -import useAppConfig from "../useAppConfig"; +import useAppConfig from '../useAppConfig'; import { addStatReading, statKeys } from '../plugin/clientStats'; import { getUser, updateUser } from '../commHelper'; -import { logDebug } from "../plugin/logger"; -import { DateTime } from "luxon"; +import { logDebug } from '../plugin/logger'; +import { DateTime } from 'luxon'; import i18next from 'i18next'; let scheduledPromise = new Promise((rs) => rs()); @@ -14,36 +14,38 @@ let isScheduling = false; // like python range() function range(start, stop, step) { - let a = [start], b = start; - while (b < stop) - a.push(b += step || 1); - return a; + let a = [start], + b = start; + while (b < stop) a.push((b += step || 1)); + return a; } // returns an array of moment objects, for all times that notifications should be sent const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { - const notifTimes = []; - for (const s of scheme.schedule) { - // the days to send notifications, as integers, relative to day zero - const notifDays = range(s.start, s.end, s.intervalInDays); - for (const d of notifDays) { - const date = DateTime.fromFormat(dayZeroDate, 'yyyy-MM-dd').plus({ days: d}).toFormat('yyyy-MM-dd') - const notifTime = DateTime.fromFormat(date+' '+timeOfDay, 'yyyy-MM-dd HH:mm'); - notifTimes.push(notifTime); - } + const notifTimes = []; + for (const s of scheme.schedule) { + // the days to send notifications, as integers, relative to day zero + const notifDays = range(s.start, s.end, s.intervalInDays); + for (const d of notifDays) { + const date = DateTime.fromFormat(dayZeroDate, 'yyyy-MM-dd') + .plus({ days: d }) + .toFormat('yyyy-MM-dd'); + const notifTime = DateTime.fromFormat(date + ' ' + timeOfDay, 'yyyy-MM-dd HH:mm'); + notifTimes.push(notifTime); } - return notifTimes; -} + } + return notifTimes; +}; // returns true if all expected times are already scheduled const areAlreadyScheduled = (notifs, expectedTimes) => { - for (const t of expectedTimes) { - if (!notifs.some((n) => DateTime.fromMillis(n.trigger.at).equals(t))) { - return false; - } + for (const t of expectedTimes) { + if (!notifs.some((n) => DateTime.fromMillis(n.trigger.at).equals(t))) { + return false; } - return true; -} + } + return true; +}; /* remove notif actions as they do not work, can restore post routing migration */ // const setUpActions = () => { @@ -57,161 +59,165 @@ const areAlreadyScheduled = (notifs, expectedTimes) => { // }); // } function debugGetScheduled(prefix) { - window['cordova'].plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length) - return logDebug(`${prefix}, there are no scheduled notifications`); - const time = DateTime.fromMillis(notifs?.[0].trigger.at).toFormat('HH:mm'); - //was in plugin, changed to scheduler - scheduledNotifs = notifs.map((n) => { - const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); - const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); - return { - key: date, - val: time - } - }); - //have the list of scheduled show up in this log - logDebug(`${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduledNotifs[0].key} at ${scheduledNotifs[0].val}`); + window['cordova'].plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length) return logDebug(`${prefix}, there are no scheduled notifications`); + const time = DateTime.fromMillis(notifs?.[0].trigger.at).toFormat('HH:mm'); + //was in plugin, changed to scheduler + scheduledNotifs = notifs.map((n) => { + const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); + const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + return { + key: date, + val: time, + }; }); + //have the list of scheduled show up in this log + logDebug( + `${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduledNotifs[0].key} at ${scheduledNotifs[0].val}`, + ); + }); } //new method to fetch notifications -const getScheduledNotifs = function() { - return new Promise((resolve, reject) => { - /* if the notifications are still in active scheduling it causes problems +const getScheduledNotifs = function () { + return new Promise((resolve, reject) => { + /* if the notifications are still in active scheduling it causes problems anywhere from 0-n of the scheduled notifs are displayed if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors */ - if(isScheduling) - { - console.log("requesting fetch while still actively scheduling, waiting on scheduledPromise"); - scheduledPromise.then(() => { - getNotifs().then((notifs) => { - console.log("done scheduling notifs", notifs); - resolve(notifs); - }) - }) - } - else{ - getNotifs().then((notifs) => { - resolve(notifs); - }) - } - }) -} + if (isScheduling) { + console.log('requesting fetch while still actively scheduling, waiting on scheduledPromise'); + scheduledPromise.then(() => { + getNotifs().then((notifs) => { + console.log('done scheduling notifs', notifs); + resolve(notifs); + }); + }); + } else { + getNotifs().then((notifs) => { + resolve(notifs); + }); + } + }); +}; //get scheduled notifications from cordova plugin and format them -const getNotifs = function() { - return new Promise((resolve, reject) => { - window['cordova'].plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length){ - console.log("there are no notifications"); - resolve([]); //if none, return empty array - } - - const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing - let scheduledNotifs = []; - scheduledNotifs = notifSubset.map((n) => { - const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); - const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); - return { - key: date, - val: time - } - }); - resolve(scheduledNotifs); - }); - }) -} +const getNotifs = function () { + return new Promise((resolve, reject) => { + window['cordova'].plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length) { + console.log('there are no notifications'); + resolve([]); //if none, return empty array + } + + const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing + let scheduledNotifs = []; + scheduledNotifs = notifSubset.map((n) => { + const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); + const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + return { + key: date, + val: time, + }; + }); + resolve(scheduledNotifs); + }); + }); +}; // schedules the notifications using the cordova plugin const scheduleNotifs = (scheme, notifTimes) => { - return new Promise((rs) => { - isScheduling = true; - const localeCode = i18next.resolvedLanguage; - console.error("notifTimes: ", notifTimes, " - type: ", typeof(notifTimes)); - const nots = notifTimes.map((n) => { - console.error("n: ", n, " - type: ", typeof(n)); - const nDate = n.toDate(); - const seconds = nDate.getTime() / 1000; - return { - id: seconds, - title: scheme.title[localeCode], - text: scheme.text[localeCode], - trigger: {at: nDate}, - // actions: 'reminder-actions', - // data: { - // action: { - // redirectTo: 'root.main.control', - // redirectParams: { - // openTimeOfDayPicker: true - // } - // } - // } - } - }); - window['cordova'].plugins.notification.local.cancelAll(() => { - debugGetScheduled("After cancelling"); - window['cordova'].plugins.notification.local.schedule(nots, () => { - debugGetScheduled("After scheduling"); - isScheduling = false; - rs(); //scheduling promise resolved here - }); - }); + return new Promise((rs) => { + isScheduling = true; + const localeCode = i18next.resolvedLanguage; + console.error('notifTimes: ', notifTimes, ' - type: ', typeof notifTimes); + const nots = notifTimes.map((n) => { + console.error('n: ', n, ' - type: ', typeof n); + const nDate = n.toDate(); + const seconds = nDate.getTime() / 1000; + return { + id: seconds, + title: scheme.title[localeCode], + text: scheme.text[localeCode], + trigger: { at: nDate }, + // actions: 'reminder-actions', + // data: { + // action: { + // redirectTo: 'root.main.control', + // redirectParams: { + // openTimeOfDayPicker: true + // } + // } + // } + }; }); -} + window['cordova'].plugins.notification.local.cancelAll(() => { + debugGetScheduled('After cancelling'); + window['cordova'].plugins.notification.local.schedule(nots, () => { + debugGetScheduled('After scheduling'); + isScheduling = false; + rs(); //scheduling promise resolved here + }); + }); + }); +}; // determines when notifications are needed, and schedules them if not already scheduled const update = async (reminderSchemes) => { - const { reminder_assignment, - reminder_join_date, - reminder_time_of_day} = await getReminderPrefs(reminderSchemes); - var scheme = {}; - try { - scheme = reminderSchemes[reminder_assignment]; - } catch (e) { - console.log("ERROR: Could not find reminder scheme for assignment " + reminderSchemes + " - " + reminder_assignment); - } - const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); - return new Promise((resolve, reject) => { - window['cordova'].plugins.notification.local.getScheduled((notifs) => { - if (areAlreadyScheduled(notifs, notifTimes)) { - logDebug("Already scheduled, not scheduling again"); - } else { - // to ensure we don't overlap with the last scheduling() request, - // we'll wait for the previous one to finish before scheduling again - scheduledPromise.then(() => { - if (isScheduling) { - console.log("ERROR: Already scheduling notifications, not scheduling again") - } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); - //enforcing end of scheduling to conisder update through - scheduledPromise.then(() => { - resolve(); - }) - } - }); - } + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = await getReminderPrefs( + reminderSchemes, + ); + var scheme = {}; + try { + scheme = reminderSchemes[reminder_assignment]; + } catch (e) { + console.log( + 'ERROR: Could not find reminder scheme for assignment ' + + reminderSchemes + + ' - ' + + reminder_assignment, + ); + } + const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); + return new Promise((resolve, reject) => { + window['cordova'].plugins.notification.local.getScheduled((notifs) => { + if (areAlreadyScheduled(notifs, notifTimes)) { + logDebug('Already scheduled, not scheduling again'); + } else { + // to ensure we don't overlap with the last scheduling() request, + // we'll wait for the previous one to finish before scheduling again + scheduledPromise.then(() => { + if (isScheduling) { + console.log('ERROR: Already scheduling notifications, not scheduling again'); + } else { + scheduledPromise = scheduleNotifs(scheme, notifTimes); + //enforcing end of scheduling to conisder update through + scheduledPromise.then(() => { + resolve(); + }); + } }); + } }); -} + }); +}; /* Randomly assign a scheme, set the join date to today, and use the default time of day from config (or noon if not specified) This is only called once when the user first joins the study */ const initReminderPrefs = (reminderSchemes) => { - // randomly assign from the schemes listed in config - const schemes = Object.keys(reminderSchemes); - const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; - const todayDate = DateTime.local().toFormat('yyyy-MM-dd'); - const defaultTime = reminderSchemes[randAssignment]?.defaultTime || '12:00'; - return { - reminder_assignment: randAssignment, - reminder_join_date: todayDate, - reminder_time_of_day: defaultTime, - }; -} + // randomly assign from the schemes listed in config + const schemes = Object.keys(reminderSchemes); + const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; + const todayDate = DateTime.local().toFormat('yyyy-MM-dd'); + const defaultTime = reminderSchemes[randAssignment]?.defaultTime || '12:00'; + return { + reminder_assignment: randAssignment, + reminder_join_date: todayDate, + reminder_time_of_day: defaultTime, + }; +}; /* EXAMPLE VALUES - present in user profile object reminder_assignment: 'passive', @@ -225,62 +231,58 @@ const initReminderPrefs = (reminderSchemes) => { // } const getReminderPrefs = async (reminderSchemes): Promise => { - const user = await getUser(); - if (user?.reminder_assignment && - user?.reminder_join_date && - user?.reminder_time_of_day) { - console.log("User already has reminder prefs, returning them", user) - return user; - } - // if no prefs, user just joined, so initialize them - console.log("User just joined, Initializing reminder prefs") - const initPrefs = initReminderPrefs(reminderSchemes); - console.log("Initialized reminder prefs: ", initPrefs); - await setReminderPrefs(initPrefs, reminderSchemes); - return { ...user, ...initPrefs }; // user profile + the new prefs -} + const user = await getUser(); + if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { + console.log('User already has reminder prefs, returning them', user); + return user; + } + // if no prefs, user just joined, so initialize them + console.log('User just joined, Initializing reminder prefs'); + const initPrefs = initReminderPrefs(reminderSchemes); + console.log('Initialized reminder prefs: ', initPrefs); + await setReminderPrefs(initPrefs, reminderSchemes); + return { ...user, ...initPrefs }; // user profile + the new prefs +}; const setReminderPrefs = async (newPrefs, reminderSchemes) => { - await updateUser(newPrefs) - const updatePromise = new Promise((resolve, reject) => { - //enforcing update before moving on - update(reminderSchemes).then(() => { - resolve(); - }); - }); - // record the new prefs in client stats - getReminderPrefs(reminderSchemes).then((prefs) => { - // extract only the relevant fields from the prefs, - // and add as a reading to client stats - const { reminder_assignment, - reminder_join_date, - reminder_time_of_day} = prefs; - addStatReading(statKeys.REMINDER_PREFS, { - reminder_assignment, - reminder_join_date, - reminder_time_of_day - }).then(logDebug("Added reminder prefs to client stats")); + await updateUser(newPrefs); + const updatePromise = new Promise((resolve, reject) => { + //enforcing update before moving on + update(reminderSchemes).then(() => { + resolve(); }); - return updatePromise; -} + }); + // record the new prefs in client stats + getReminderPrefs(reminderSchemes).then((prefs) => { + // extract only the relevant fields from the prefs, + // and add as a reading to client stats + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; + addStatReading(statKeys.REMINDER_PREFS, { + reminder_assignment, + reminder_join_date, + reminder_time_of_day, + }).then(logDebug('Added reminder prefs to client stats')); + }); + return updatePromise; +}; export function useSchedulerHelper() { - const appConfig = useAppConfig(); - const [reminderSchemes, setReminderSchemes] = useState(); + const appConfig = useAppConfig(); + const [reminderSchemes, setReminderSchemes] = useState(); - useEffect(() => { - if (!appConfig) { - logDebug("No reminder schemes found in config, not scheduling notifications"); - return; - } - setReminderSchemes(appConfig.reminderSchemes); - }, [appConfig]); + useEffect(() => { + if (!appConfig) { + logDebug('No reminder schemes found in config, not scheduling notifications'); + return; + } + setReminderSchemes(appConfig.reminderSchemes); + }, [appConfig]); - //setUpActions(); - update(reminderSchemes); + //setUpActions(); + update(reminderSchemes); - return { - setReminderPrefs: (newPrefs) => setReminderPrefs(newPrefs, reminderSchemes), - getReminderPrefs: () => getReminderPrefs(reminderSchemes), - getScheduledNotifs: () => getScheduledNotifs(), - } -} \ No newline at end of file + return { + setReminderPrefs: (newPrefs) => setReminderPrefs(newPrefs, reminderSchemes), + getReminderPrefs: () => getReminderPrefs(reminderSchemes), + getScheduledNotifs: () => getScheduledNotifs(), + }; +} From 39986a35f3430077b3ddf3aa3455cf9af49c9777 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 2 Nov 2023 17:02:23 -0600 Subject: [PATCH 03/36] Temporary any type, handled purely in notifScheduler getReminderPrefs In order to not mess with commHelper.ts as it is outside of the scope of this issue - related to https://github.com/e-mission/e-mission-phone/pull/1092#discussion_r1379114750 --- www/js/commHelper.ts | 2 +- www/js/splash/notifScheduler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index e39bed841..5f144888b 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -196,7 +196,7 @@ export function updateUser(updateDoc) { }); } -export function getUser(): any { +export function getUser() { return new Promise((rs, rj) => { window['cordova'].plugins.BEMServerComm.getUserPersonalData('/profile/get', rs, rj); }).catch((error) => { diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index cd3385ff8..6b054b7af 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -231,7 +231,7 @@ const initReminderPrefs = (reminderSchemes) => { // } const getReminderPrefs = async (reminderSchemes): Promise => { - const user = await getUser(); + const user = (await getUser()) as any; if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { console.log('User already has reminder prefs, returning them', user); return user; From 58539cad0c34ab8568bc0b3d5002be0ee334a071 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 2 Nov 2023 17:06:18 -0600 Subject: [PATCH 04/36] Undoing imports getting duplicated during prettier changes During the prettier changes, a bunch of the imports got merged. Undoing in this commit: https://github.com/e-mission/e-mission-phone/pull/1092/commits/1bfb113605f161c16dcdf28b73233b18c76f3cb0 --- www/js/control/ProfileSettings.jsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index b57111f51..b081e642a 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -33,20 +33,6 @@ import { storageClear } from '../plugin/storage'; import { getAppVersion } from '../plugin/clientStats'; import { getConsentDocument } from '../splash/startprefs'; import { logDebug } from '../plugin/logger'; -import {uploadFile} from "./uploadService"; -import ActionMenu from "../components/ActionMenu"; -import SensedPage from "./SensedPage" -import LogPage from "./LogPage"; -import ControlSyncHelper, {ForceSyncRow, getHelperSyncSettings} from "./ControlSyncHelper"; -import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransition} from "./ControlCollectionHelper"; -import { resetDataAndRefresh } from "../config/dynamicConfig"; -import { AppContext } from "../App"; -import { shareQR } from "../components/QrCode"; -import { storageClear } from "../plugin/storage"; -import { getAppVersion } from "../plugin/clientStats"; -import { useSchedulerHelper } from "../splash/notifScheduler"; -import { getConsentDocument } from "../splash/startprefs"; -import { logDebug } from "../plugin/logger"; //any pure functions can go outside const ProfileSettings = () => { From 384ffe8e6d053575bf3c90a766c0da630ebcb95b Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 2 Nov 2023 17:08:47 -0600 Subject: [PATCH 05/36] Replace console.log with logger.ts's displayErrorMsg Per https://github.com/e-mission/e-mission-phone/pull/1092#discussion_r1379143510 --- www/js/splash/notifScheduler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 6b054b7af..020a864d7 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -4,7 +4,7 @@ import { getConfig } from '../config/dynamicConfig'; import useAppConfig from '../useAppConfig'; import { addStatReading, statKeys } from '../plugin/clientStats'; import { getUser, updateUser } from '../commHelper'; -import { logDebug } from '../plugin/logger'; +import { displayErrorMsg, logDebug } from '../plugin/logger'; import { DateTime } from 'luxon'; import i18next from 'i18next'; @@ -171,7 +171,7 @@ const update = async (reminderSchemes) => { try { scheme = reminderSchemes[reminder_assignment]; } catch (e) { - console.log( + displayErrorMsg( 'ERROR: Could not find reminder scheme for assignment ' + reminderSchemes + ' - ' + From 9d3c718f3e2e1aa375837c291b480b77681b789a Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 3 Nov 2023 15:59:07 -0600 Subject: [PATCH 06/36] Move scheduledNotifs into the only function that uses it --- www/js/control/ProfileSettings.jsx | 18 +++++++++++++----- www/js/splash/notifScheduler.ts | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index b081e642a..695201a33 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -32,7 +32,8 @@ import { shareQR } from '../components/QrCode'; import { storageClear } from '../plugin/storage'; import { getAppVersion } from '../plugin/clientStats'; import { getConsentDocument } from '../splash/startprefs'; -import { logDebug } from '../plugin/logger'; +import { displayErrorMsg, logDebug } from '../plugin/logger'; +import { updateScheduledNotifs, getScheduledNotifs, getReminderPrefs, setReminderPrefs } from "../splash/notifScheduler"; //any pure functions can go outside const ProfileSettings = () => { @@ -45,7 +46,6 @@ const ProfileSettings = () => { //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); const EmailHelper = getAngularService('EmailHelper'); - const NotificationScheduler = getAngularService('NotificationScheduler'); const ControlHelper = getAngularService('ControlHelper'); //functions that come directly from an Angular service @@ -141,6 +141,14 @@ const ProfileSettings = () => { tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; } + // Update the scheduled notifs + updateScheduledNotifs(tempUiConfig.reminderSchemes).then(() => { + logDebug("updated scheduled notifs"); + }) + .catch((err) => { + displayErrorMsg("Error while updating scheduled notifs", err); + }); + // setTemplateText(tempUiConfig.intro.translated_text); // console.log("translated text is??", templateText); setUiConfig(tempUiConfig); @@ -187,13 +195,13 @@ const ProfileSettings = () => { const newNotificationSettings = {}; if (uiConfig?.reminderSchemes) { - const prefs = await NotificationScheduler.getReminderPrefs(); + const prefs = await getReminderPrefs(); const m = moment(prefs.reminder_time_of_day, 'HH:mm'); newNotificationSettings.prefReminderTimeVal = m.toDate(); const n = moment(newNotificationSettings.prefReminderTimeVal); newNotificationSettings.prefReminderTime = n.format('LT'); newNotificationSettings.prefReminderTimeOnLoad = prefs.reminder_time_of_day; - newNotificationSettings.scheduledNotifs = await NotificationScheduler.getScheduledNotifs(); + newNotificationSettings.scheduledNotifs = await getScheduledNotifs(); updatePrefReminderTime(false); } @@ -264,7 +272,7 @@ const ProfileSettings = () => { if (storeNewVal) { const m = moment(newTime); // store in HH:mm - NotificationScheduler.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }).then( + setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }, uiConfig.reminderSchemes).then( () => { refreshNotificationSettings(); }, diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 020a864d7..f4358014a 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -9,7 +9,6 @@ import { DateTime } from 'luxon'; import i18next from 'i18next'; let scheduledPromise = new Promise((rs) => rs()); -let scheduledNotifs = []; let isScheduling = false; // like python range() @@ -63,6 +62,7 @@ function debugGetScheduled(prefix) { if (!notifs?.length) return logDebug(`${prefix}, there are no scheduled notifications`); const time = DateTime.fromMillis(notifs?.[0].trigger.at).toFormat('HH:mm'); //was in plugin, changed to scheduler + let scheduledNotifs = []; scheduledNotifs = notifs.map((n) => { const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); From a1e035fd1d21c8d5df269faa1a1a850d9e73ac17 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 3 Nov 2023 15:59:31 -0600 Subject: [PATCH 07/36] Remove console.errors that I was using for debugging --- www/js/splash/notifScheduler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index f4358014a..56deab160 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -130,9 +130,7 @@ const scheduleNotifs = (scheme, notifTimes) => { return new Promise((rs) => { isScheduling = true; const localeCode = i18next.resolvedLanguage; - console.error('notifTimes: ', notifTimes, ' - type: ', typeof notifTimes); const nots = notifTimes.map((n) => { - console.error('n: ', n, ' - type: ', typeof n); const nDate = n.toDate(); const seconds = nDate.getTime() / 1000; return { From 8b09cb331ebdb4135405f06ac219e406b2e8e840 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 3 Nov 2023 15:59:53 -0600 Subject: [PATCH 08/36] Adding Luxon functions --- www/js/splash/notifScheduler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 56deab160..723b949ca 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -131,8 +131,8 @@ const scheduleNotifs = (scheme, notifTimes) => { isScheduling = true; const localeCode = i18next.resolvedLanguage; const nots = notifTimes.map((n) => { - const nDate = n.toDate(); - const seconds = nDate.getTime() / 1000; + const nDate = n.toISO(); + const seconds = nDate.ts / 1000; return { id: seconds, title: scheme.title[localeCode], From 96d1d50bae35157f9d61d36384a45e3fef725482 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 3 Nov 2023 16:01:50 -0600 Subject: [PATCH 09/36] Update notifscheduler to export functions instead of using hooks --- www/js/splash/notifScheduler.ts | 38 ++++++--------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 723b949ca..e41430529 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -1,6 +1,5 @@ import angular from 'angular'; import React, { useEffect, useState } from 'react'; -import { getConfig } from '../config/dynamicConfig'; import useAppConfig from '../useAppConfig'; import { addStatReading, statKeys } from '../plugin/clientStats'; import { getUser, updateUser } from '../commHelper'; @@ -79,7 +78,7 @@ function debugGetScheduled(prefix) { } //new method to fetch notifications -const getScheduledNotifs = function () { +export const getScheduledNotifs = function () { return new Promise((resolve, reject) => { /* if the notifications are still in active scheduling it causes problems anywhere from 0-n of the scheduled notifs are displayed @@ -161,10 +160,9 @@ const scheduleNotifs = (scheme, notifTimes) => { }; // determines when notifications are needed, and schedules them if not already scheduled -const update = async (reminderSchemes) => { - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = await getReminderPrefs( - reminderSchemes, - ); +export const updateScheduledNotifs = async (reminderSchemes): Promise => { + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = + await getReminderPrefs(reminderSchemes); var scheme = {}; try { scheme = reminderSchemes[reminder_assignment]; @@ -228,7 +226,7 @@ const initReminderPrefs = (reminderSchemes) => { // reminder_time_of_day: string; // } -const getReminderPrefs = async (reminderSchemes): Promise => { +export const getReminderPrefs = async (reminderSchemes): Promise => { const user = (await getUser()) as any; if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { console.log('User already has reminder prefs, returning them', user); @@ -241,11 +239,11 @@ const getReminderPrefs = async (reminderSchemes): Promise => { await setReminderPrefs(initPrefs, reminderSchemes); return { ...user, ...initPrefs }; // user profile + the new prefs }; -const setReminderPrefs = async (newPrefs, reminderSchemes) => { +export const setReminderPrefs = async (newPrefs, reminderSchemes) => { await updateUser(newPrefs); const updatePromise = new Promise((resolve, reject) => { //enforcing update before moving on - update(reminderSchemes).then(() => { + updateScheduledNotifs(reminderSchemes).then(() => { resolve(); }); }); @@ -262,25 +260,3 @@ const setReminderPrefs = async (newPrefs, reminderSchemes) => { }); return updatePromise; }; - -export function useSchedulerHelper() { - const appConfig = useAppConfig(); - const [reminderSchemes, setReminderSchemes] = useState(); - - useEffect(() => { - if (!appConfig) { - logDebug('No reminder schemes found in config, not scheduling notifications'); - return; - } - setReminderSchemes(appConfig.reminderSchemes); - }, [appConfig]); - - //setUpActions(); - update(reminderSchemes); - - return { - setReminderPrefs: (newPrefs) => setReminderPrefs(newPrefs, reminderSchemes), - getReminderPrefs: () => getReminderPrefs(reminderSchemes), - getScheduledNotifs: () => getScheduledNotifs(), - }; -} From a6d99e736906727cb989e7478d8fccff3fc0ace1 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 3 Nov 2023 16:03:10 -0600 Subject: [PATCH 10/36] Run prettier on non-pretty code --- www/js/control/ProfileSettings.jsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 695201a33..a056470a4 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -33,7 +33,12 @@ import { storageClear } from '../plugin/storage'; import { getAppVersion } from '../plugin/clientStats'; import { getConsentDocument } from '../splash/startprefs'; import { displayErrorMsg, logDebug } from '../plugin/logger'; -import { updateScheduledNotifs, getScheduledNotifs, getReminderPrefs, setReminderPrefs } from "../splash/notifScheduler"; +import { + updateScheduledNotifs, + getScheduledNotifs, + getReminderPrefs, + setReminderPrefs, +} from '../splash/notifScheduler'; //any pure functions can go outside const ProfileSettings = () => { @@ -142,12 +147,13 @@ const ProfileSettings = () => { } // Update the scheduled notifs - updateScheduledNotifs(tempUiConfig.reminderSchemes).then(() => { - logDebug("updated scheduled notifs"); - }) - .catch((err) => { - displayErrorMsg("Error while updating scheduled notifs", err); - }); + updateScheduledNotifs(tempUiConfig.reminderSchemes) + .then(() => { + logDebug('updated scheduled notifs'); + }) + .catch((err) => { + displayErrorMsg('Error while updating scheduled notifs', err); + }); // setTemplateText(tempUiConfig.intro.translated_text); // console.log("translated text is??", templateText); From b0da6e90f47c55ba4e1be5e8657397d8c8b568b4 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 7 Nov 2023 09:07:18 -0700 Subject: [PATCH 11/36] Remove old Angular notifScheduler.js --- www/index.js | 1 - www/js/main.js | 1 - www/js/splash/notifScheduler.js | 265 -------------------------------- 3 files changed, 267 deletions(-) delete mode 100644 www/js/splash/notifScheduler.js diff --git a/www/index.js b/www/index.js index 78d29cf7a..6adc62186 100644 --- a/www/index.js +++ b/www/index.js @@ -9,7 +9,6 @@ import './js/splash/pushnotify.js'; import './js/splash/storedevicesettings.js'; import './js/splash/localnotify.js'; import './js/splash/remotenotify.js'; -import './js/splash/notifScheduler.js'; import './js/controllers.js'; import './js/services.js'; import './js/i18n-utils.js'; diff --git a/www/js/main.js b/www/js/main.js index 2b351e2c4..ce245cf98 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -6,7 +6,6 @@ angular .module('emission.main', [ 'emission.main.diary', 'emission.i18n.utils', - 'emission.splash.notifscheduler', 'emission.main.metrics.factory', 'emission.main.metrics.mappings', 'emission.services', diff --git a/www/js/splash/notifScheduler.js b/www/js/splash/notifScheduler.js deleted file mode 100644 index 9ceb0a23e..000000000 --- a/www/js/splash/notifScheduler.js +++ /dev/null @@ -1,265 +0,0 @@ -'use strict'; - -import angular from 'angular'; -import { getConfig } from '../config/dynamicConfig'; -import { addStatReading, statKeys } from '../plugin/clientStats'; -import { getUser, updateUser } from '../commHelper'; - -angular - .module('emission.splash.notifscheduler', ['emission.services', 'emission.plugin.logger']) - - .factory('NotificationScheduler', function ($http, $window, $ionicPlatform, Logger) { - const scheduler = {}; - let _config; - let scheduledPromise = new Promise((rs) => rs()); - let isScheduling = false; - - // like python range() - function range(start, stop, step) { - let a = [start], - b = start; - while (b < stop) a.push((b += step || 1)); - return a; - } - - // returns an array of moment objects, for all times that notifications should be sent - const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { - const notifTimes = []; - for (const s of scheme.schedule) { - // the days to send notifications, as integers, relative to day zero - const notifDays = range(s.start, s.end, s.intervalInDays); - for (const d of notifDays) { - const date = moment(dayZeroDate).add(d, 'days').format('YYYY-MM-DD'); - const notifTime = moment(date + ' ' + timeOfDay, 'YYYY-MM-DD HH:mm'); - notifTimes.push(notifTime); - } - } - return notifTimes; - }; - - // returns true if all expected times are already scheduled - const areAlreadyScheduled = (notifs, expectedTimes) => { - for (const t of expectedTimes) { - if (!notifs.some((n) => moment(n.at).isSame(t))) { - return false; - } - } - return true; - }; - - /* remove notif actions as they do not work, can restore post routing migration */ - // const setUpActions = () => { - // const action = { - // id: 'action', - // title: 'Change Time', - // launch: true - // }; - // return new Promise((rs) => { - // cordova.plugins.notification.local.addActions('reminder-actions', [action], rs); - // }); - // } - - function debugGetScheduled(prefix) { - cordova.plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length) return Logger.log(`${prefix}, there are no scheduled notifications`); - const time = moment(notifs?.[0].trigger.at).format('HH:mm'); - //was in plugin, changed to scheduler - scheduler.scheduledNotifs = notifs.map((n) => { - const time = moment(n.trigger.at).format('LT'); - const date = moment(n.trigger.at).format('LL'); - return { - key: date, - val: time, - }; - }); - //have the list of scheduled show up in this log - Logger.log( - `${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduler.scheduledNotifs[0].key} at ${scheduler.scheduledNotifs[0].val}`, - ); - }); - } - - //new method to fetch notifications - scheduler.getScheduledNotifs = function () { - return new Promise((resolve, reject) => { - /* if the notifications are still in active scheduling it causes problems - anywhere from 0-n of the scheduled notifs are displayed - if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors - */ - if (isScheduling) { - console.log( - 'requesting fetch while still actively scheduling, waiting on scheduledPromise', - ); - scheduledPromise.then(() => { - getNotifs().then((notifs) => { - console.log('done scheduling notifs', notifs); - resolve(notifs); - }); - }); - } else { - getNotifs().then((notifs) => { - resolve(notifs); - }); - } - }); - }; - - //get scheduled notifications from cordova plugin and format them - const getNotifs = function () { - return new Promise((resolve, reject) => { - cordova.plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length) { - console.log('there are no notifications'); - resolve([]); //if none, return empty array - } - - const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing - let scheduledNotifs = []; - scheduledNotifs = notifSubset.map((n) => { - const time = moment(n.trigger.at).format('LT'); - const date = moment(n.trigger.at).format('LL'); - return { - key: date, - val: time, - }; - }); - resolve(scheduledNotifs); - }); - }); - }; - - // schedules the notifications using the cordova plugin - const scheduleNotifs = (scheme, notifTimes) => { - return new Promise((rs) => { - isScheduling = true; - const localeCode = i18next.resolvedLanguage; - const nots = notifTimes.map((n) => { - const nDate = n.toDate(); - const seconds = nDate.getTime() / 1000; - return { - id: seconds, - title: scheme.title[localeCode], - text: scheme.text[localeCode], - trigger: { at: nDate }, - // actions: 'reminder-actions', - // data: { - // action: { - // redirectTo: 'root.main.control', - // redirectParams: { - // openTimeOfDayPicker: true - // } - // } - // } - }; - }); - cordova.plugins.notification.local.cancelAll(() => { - debugGetScheduled('After cancelling'); - cordova.plugins.notification.local.schedule(nots, () => { - debugGetScheduled('After scheduling'); - isScheduling = false; - rs(); //scheduling promise resolved here - }); - }); - }); - }; - - // determines when notifications are needed, and schedules them if not already scheduled - const update = async () => { - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = - await scheduler.getReminderPrefs(); - const scheme = _config.reminderSchemes[reminder_assignment]; - const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); - - return new Promise((resolve, reject) => { - cordova.plugins.notification.local.getScheduled((notifs) => { - if (areAlreadyScheduled(notifs, notifTimes)) { - Logger.log('Already scheduled, not scheduling again'); - } else { - // to ensure we don't overlap with the last scheduling() request, - // we'll wait for the previous one to finish before scheduling again - scheduledPromise.then(() => { - if (isScheduling) { - console.log('ERROR: Already scheduling notifications, not scheduling again'); - } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); - //enforcing end of scheduling to conisder update through - scheduledPromise.then(() => { - resolve(); - }); - } - }); - } - }); - }); - }; - - /* Randomly assign a scheme, set the join date to today, - and use the default time of day from config (or noon if not specified) - This is only called once when the user first joins the study - */ - const initReminderPrefs = () => { - // randomly assign from the schemes listed in config - const schemes = Object.keys(_config.reminderSchemes); - const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; - const todayDate = moment().format('YYYY-MM-DD'); - const defaultTime = _config.reminderSchemes[randAssignment]?.defaultTime || '12:00'; - return { - reminder_assignment: randAssignment, - reminder_join_date: todayDate, - reminder_time_of_day: defaultTime, - }; - }; - - /* EXAMPLE VALUES - present in user profile object - reminder_assignment: 'passive', - reminder_join_date: '2023-05-09', - reminder_time_of_day: '21:00', - */ - - scheduler.getReminderPrefs = async () => { - const user = await getUser(); - if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { - return user; - } - // if no prefs, user just joined, so initialize them - const initPrefs = initReminderPrefs(); - await scheduler.setReminderPrefs(initPrefs); - return { ...user, ...initPrefs }; // user profile + the new prefs - }; - - scheduler.setReminderPrefs = async (newPrefs) => { - await updateUser(newPrefs); - const updatePromise = new Promise((resolve, reject) => { - //enforcing update before moving on - update().then(() => { - resolve(); - }); - }); - - // record the new prefs in client stats - scheduler.getReminderPrefs().then((prefs) => { - // extract only the relevant fields from the prefs, - // and add as a reading to client stats - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; - addStatReading(statKeys.REMINDER_PREFS, { - reminder_assignment, - reminder_join_date, - reminder_time_of_day, - }).then(Logger.log('Added reminder prefs to client stats')); - }); - - return updatePromise; - }; - - $ionicPlatform.ready().then(async () => { - _config = await getConfig(); - if (!_config.reminderSchemes) { - Logger.log('No reminder schemes found in config, not scheduling notifications'); - return; - } - //setUpActions(); - update(); - }); - - return scheduler; - }); From 6b9d222fa0b11e6dd06711c8913cbd997b829c21 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 7 Nov 2023 09:07:48 -0700 Subject: [PATCH 12/36] Add the reminderSchemes as an argument to called getReminderPrefs --- www/js/control/ProfileSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index a056470a4..1885c5a9e 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -201,7 +201,7 @@ const ProfileSettings = () => { const newNotificationSettings = {}; if (uiConfig?.reminderSchemes) { - const prefs = await getReminderPrefs(); + const prefs = await getReminderPrefs(uiConfig.reminderSchemes); const m = moment(prefs.reminder_time_of_day, 'HH:mm'); newNotificationSettings.prefReminderTimeVal = m.toDate(); const n = moment(newNotificationSettings.prefReminderTimeVal); From 97571eeb8573edf56ea61f0a17ca49a71a80ef31 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 7 Nov 2023 13:28:26 -0700 Subject: [PATCH 13/36] Added typing for User object in getReminderPrefs notifScheduler.ts - Added a User interface that acts as a structure instead of using any for the type --- www/js/splash/notifScheduler.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index e41430529..906d0712b 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -226,8 +226,15 @@ const initReminderPrefs = (reminderSchemes) => { // reminder_time_of_day: string; // } -export const getReminderPrefs = async (reminderSchemes): Promise => { - const user = (await getUser()) as any; +interface User { + reminder_assignment: string; + reminder_join_date: string; + reminder_time_of_day: string; +} + +export const getReminderPrefs = async (reminderSchemes): Promise => { + const userPromise = getUser(); + const user = (await userPromise) as User; if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { console.log('User already has reminder prefs, returning them', user); return user; From a29705d891ecff154c9cf5a87a31b84fd3fbdef0 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 7 Nov 2023 13:30:43 -0700 Subject: [PATCH 14/36] Restructure promise format and replaced moment with Luxon ProfileSettings.tsx - 2 locations that used moment I replaced with Luxon because the notifScheduler is returning DateTime objects which don't mesh with moment - Restructured the if(uiConfig?.reminderSchemes) in refreshNotificationSettings to execute the promises at first, and then set the data - Some Prettier formatting took over here automatically too --- www/js/control/ProfileSettings.jsx | 33 +++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 1885c5a9e..492337de0 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -39,6 +39,7 @@ import { getReminderPrefs, setReminderPrefs, } from '../splash/notifScheduler'; +import { DateTime } from 'luxon'; //any pure functions can go outside const ProfileSettings = () => { @@ -201,13 +202,20 @@ const ProfileSettings = () => { const newNotificationSettings = {}; if (uiConfig?.reminderSchemes) { - const prefs = await getReminderPrefs(uiConfig.reminderSchemes); - const m = moment(prefs.reminder_time_of_day, 'HH:mm'); - newNotificationSettings.prefReminderTimeVal = m.toDate(); - const n = moment(newNotificationSettings.prefReminderTimeVal); - newNotificationSettings.prefReminderTime = n.format('LT'); + let promiseList = []; + promiseList.push(getReminderPrefs(uiConfig.reminderSchemes)); + promiseList.push(getScheduledNotifs()); + let resultList = await Promise.all(promiseList); + const prefs = resultList[0]; + const scheduledNotifs = resultList[1]; + console.log('prefs and scheduled notifs', resultList[0], resultList[1]); + + const m = DateTime.fromFormat(prefs.reminder_time_of_day, 'HH:mm'); + newNotificationSettings.prefReminderTimeVal = m.toJSDate(); + newNotificationSettings.prefReminderTime = m.toFormat('t'); newNotificationSettings.prefReminderTimeOnLoad = prefs.reminder_time_of_day; - newNotificationSettings.scheduledNotifs = await getScheduledNotifs(); + newNotificationSettings.scheduledNotifs = scheduledNotifs; + updatePrefReminderTime(false); } @@ -276,13 +284,14 @@ const ProfileSettings = () => { async function updatePrefReminderTime(storeNewVal = true, newTime) { console.log(newTime); if (storeNewVal) { - const m = moment(newTime); + const m = DateTime.fromISO(newTime); // store in HH:mm - setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }, uiConfig.reminderSchemes).then( - () => { - refreshNotificationSettings(); - }, - ); + setReminderPrefs( + { reminder_time_of_day: m.toFormat('HH:mm') }, + uiConfig.reminderSchemes, + ).then(() => { + refreshNotificationSettings(); + }); } } From ba239e63a7b267020813a6223704c892bbff25eb Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 9 Nov 2023 09:38:38 -0700 Subject: [PATCH 15/36] Resolve the app crashing errors From https://github.com/e-mission/e-mission-phone/pull/1092#issuecomment-1800021840 notifScheduler.ts - Replaced toISO for the date object with toJSDate, per @JGreenlee suggestion: https://github.com/e-mission/e-mission-phone/pull/1092#discussion_r1387470795 --- www/js/splash/notifScheduler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 906d0712b..0bc8bd747 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -130,8 +130,8 @@ const scheduleNotifs = (scheme, notifTimes) => { isScheduling = true; const localeCode = i18next.resolvedLanguage; const nots = notifTimes.map((n) => { - const nDate = n.toISO(); - const seconds = nDate.ts / 1000; + const nDate = n.toJSDate(); + const seconds = nDate.getTime() / 1000; return { id: seconds, title: scheme.title[localeCode], From 2f75de9f4d044c999afc53a7146b72172fc2c96b Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 10 Nov 2023 11:20:39 -0700 Subject: [PATCH 16/36] Remove unnecessary imports notifScheduler.ts - Removed angular (no longer used), - React useState and useEffect (no hooks used), - and useAppConfig (pulled in as arguments in the exported functions) --- www/js/splash/notifScheduler.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 0bc8bd747..326e9fe86 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -1,6 +1,3 @@ -import angular from 'angular'; -import React, { useEffect, useState } from 'react'; -import useAppConfig from '../useAppConfig'; import { addStatReading, statKeys } from '../plugin/clientStats'; import { getUser, updateUser } from '../commHelper'; import { displayErrorMsg, logDebug } from '../plugin/logger'; From 5e572d4b4701ce58a0ef844b2329c9e5336013da Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 14 Nov 2023 10:33:23 -0700 Subject: [PATCH 17/36] notif-scheduling-state variables kept in ProfileSettings.jsx and handed as parameters ProfileSettings.jsx - Created scheduledPromise and isScheduling - Hand these variables as arguments to notifScheduler functions that require it notifScheduler - Removed scheduledPromise and isScheduling as they aren't doing anything - Added isScheduling and scheduledPromise as parameters to functions that require it - Some prettier formatting came in --- www/js/control/ProfileSettings.jsx | 12 ++++++--- www/js/splash/notifScheduler.ts | 43 +++++++++++++++++++----------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 492337de0..4cab17024 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -101,6 +101,10 @@ const ProfileSettings = () => { { text: 'Remote push', transition: 'RECEIVED_SILENT_PUSH' }, ]; + // used for scheduling notifs + let scheduledPromise = new Promise((rs) => rs()); + let isScheduling = false; + useEffect(() => { //added appConfig.name needed to be defined because appConfig was defined but empty if (appConfig && appConfig.name) { @@ -148,7 +152,7 @@ const ProfileSettings = () => { } // Update the scheduled notifs - updateScheduledNotifs(tempUiConfig.reminderSchemes) + updateScheduledNotifs(tempUiConfig.reminderSchemes, isScheduling, scheduledPromise) .then(() => { logDebug('updated scheduled notifs'); }) @@ -203,8 +207,8 @@ const ProfileSettings = () => { if (uiConfig?.reminderSchemes) { let promiseList = []; - promiseList.push(getReminderPrefs(uiConfig.reminderSchemes)); - promiseList.push(getScheduledNotifs()); + promiseList.push(getReminderPrefs(uiConfig.reminderSchemes, isScheduling, scheduledPromise)); + promiseList.push(getScheduledNotifs(isScheduling, scheduledPromise)); let resultList = await Promise.all(promiseList); const prefs = resultList[0]; const scheduledNotifs = resultList[1]; @@ -289,6 +293,8 @@ const ProfileSettings = () => { setReminderPrefs( { reminder_time_of_day: m.toFormat('HH:mm') }, uiConfig.reminderSchemes, + isScheduling, + scheduledPromise, ).then(() => { refreshNotificationSettings(); }); diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 326e9fe86..e21f61500 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -4,9 +4,6 @@ import { displayErrorMsg, logDebug } from '../plugin/logger'; import { DateTime } from 'luxon'; import i18next from 'i18next'; -let scheduledPromise = new Promise((rs) => rs()); -let isScheduling = false; - // like python range() function range(start, stop, step) { let a = [start], @@ -75,7 +72,7 @@ function debugGetScheduled(prefix) { } //new method to fetch notifications -export const getScheduledNotifs = function () { +export const getScheduledNotifs = function (isScheduling: boolean, scheduledPromise: Promise) { return new Promise((resolve, reject) => { /* if the notifications are still in active scheduling it causes problems anywhere from 0-n of the scheduled notifs are displayed @@ -122,7 +119,7 @@ const getNotifs = function () { }; // schedules the notifications using the cordova plugin -const scheduleNotifs = (scheme, notifTimes) => { +const scheduleNotifs = (scheme, notifTimes: [DateTime], isScheduling: boolean) => { return new Promise((rs) => { isScheduling = true; const localeCode = i18next.resolvedLanguage; @@ -157,9 +154,16 @@ const scheduleNotifs = (scheme, notifTimes) => { }; // determines when notifications are needed, and schedules them if not already scheduled -export const updateScheduledNotifs = async (reminderSchemes): Promise => { - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = - await getReminderPrefs(reminderSchemes); +export const updateScheduledNotifs = async ( + reminderSchemes, + isScheduling: boolean, + scheduledPromise: Promise, +): Promise => { + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = await getReminderPrefs( + reminderSchemes, + isScheduling, + scheduledPromise, + ); var scheme = {}; try { scheme = reminderSchemes[reminder_assignment]; @@ -171,7 +175,7 @@ export const updateScheduledNotifs = async (reminderSchemes): Promise => { reminder_assignment, ); } - const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); + const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day) as [DateTime]; return new Promise((resolve, reject) => { window['cordova'].plugins.notification.local.getScheduled((notifs) => { if (areAlreadyScheduled(notifs, notifTimes)) { @@ -183,7 +187,7 @@ export const updateScheduledNotifs = async (reminderSchemes): Promise => { if (isScheduling) { console.log('ERROR: Already scheduling notifications, not scheduling again'); } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); + scheduledPromise = scheduleNotifs(scheme, notifTimes, isScheduling); //enforcing end of scheduling to conisder update through scheduledPromise.then(() => { resolve(); @@ -229,7 +233,11 @@ interface User { reminder_time_of_day: string; } -export const getReminderPrefs = async (reminderSchemes): Promise => { +export const getReminderPrefs = async ( + reminderSchemes, + isScheduling: boolean, + scheduledPromise: Promise, +): Promise => { const userPromise = getUser(); const user = (await userPromise) as User; if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { @@ -240,19 +248,24 @@ export const getReminderPrefs = async (reminderSchemes): Promise => { console.log('User just joined, Initializing reminder prefs'); const initPrefs = initReminderPrefs(reminderSchemes); console.log('Initialized reminder prefs: ', initPrefs); - await setReminderPrefs(initPrefs, reminderSchemes); + await setReminderPrefs(initPrefs, reminderSchemes, isScheduling, scheduledPromise); return { ...user, ...initPrefs }; // user profile + the new prefs }; -export const setReminderPrefs = async (newPrefs, reminderSchemes) => { +export const setReminderPrefs = async ( + newPrefs, + reminderSchemes, + isScheduling: boolean, + scheduledPromise: Promise, +) => { await updateUser(newPrefs); const updatePromise = new Promise((resolve, reject) => { //enforcing update before moving on - updateScheduledNotifs(reminderSchemes).then(() => { + updateScheduledNotifs(reminderSchemes, isScheduling, scheduledPromise).then(() => { resolve(); }); }); // record the new prefs in client stats - getReminderPrefs(reminderSchemes).then((prefs) => { + getReminderPrefs(reminderSchemes, isScheduling, scheduledPromise).then((prefs) => { // extract only the relevant fields from the prefs, // and add as a reading to client stats const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; From db91c8cdae16c4cd1b1ae24d4eda1276092b89b3 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 14 Nov 2023 11:09:57 -0700 Subject: [PATCH 18/36] Remove empty objects from notifs, fixing the "n.trigger.at does not exist" error notifScheduler.ts - Added some typing - Created removeEmptyObjects to remove any empty objects from notifs coming from the cordova plugin --- www/js/splash/notifScheduler.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index e21f61500..9f1f5fd8f 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -12,9 +12,9 @@ function range(start, stop, step) { return a; } -// returns an array of moment objects, for all times that notifications should be sent -const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { - const notifTimes = []; +// returns an array of DateTime objects, for all times that notifications should be sent +const calcNotifTimes = (scheme, dayZeroDate, timeOfDay): DateTime[] => { + const notifTimes: DateTime[] = []; for (const s of scheme.schedule) { // the days to send notifications, as integers, relative to day zero const notifDays = range(s.start, s.end, s.intervalInDays); @@ -119,7 +119,7 @@ const getNotifs = function () { }; // schedules the notifications using the cordova plugin -const scheduleNotifs = (scheme, notifTimes: [DateTime], isScheduling: boolean) => { +const scheduleNotifs = (scheme, notifTimes: DateTime[], isScheduling: boolean) => { return new Promise((rs) => { isScheduling = true; const localeCode = i18next.resolvedLanguage; @@ -153,6 +153,10 @@ const scheduleNotifs = (scheme, notifTimes: [DateTime], isScheduling: boolean) = }); }; +const removeEmptyObjects = (list: any[]): any[] => { + return list.filter((n) => Object.keys(n).length !== 0); +}; + // determines when notifications are needed, and schedules them if not already scheduled export const updateScheduledNotifs = async ( reminderSchemes, @@ -175,9 +179,11 @@ export const updateScheduledNotifs = async ( reminder_assignment, ); } - const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day) as [DateTime]; + const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day) as DateTime[]; return new Promise((resolve, reject) => { window['cordova'].plugins.notification.local.getScheduled((notifs) => { + // some empty objects slip through, remove them from notifs + notifs = removeEmptyObjects(notifs); if (areAlreadyScheduled(notifs, notifTimes)) { logDebug('Already scheduled, not scheduling again'); } else { From e4817da1aa01aeadb14122d219c309e7e7a63cd1 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 14 Nov 2023 11:28:06 -0700 Subject: [PATCH 19/36] Added removeEmptyObjects check to the other cordova plugin getScheduled notifs call --- www/js/splash/notifScheduler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 9f1f5fd8f..20f1daa66 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -101,6 +101,9 @@ const getNotifs = function () { if (!notifs?.length) { console.log('there are no notifications'); resolve([]); //if none, return empty array + } else { + // some empty objects slip through, remove them from notifs + notifs = removeEmptyObjects(notifs); } const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing From 0ee468a2cd65a531c1c38f10070a79acc67f1514 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 16 Nov 2023 09:39:09 -0700 Subject: [PATCH 20/36] Added sorting to list of notifications right before they get scheduled notifScheduler.ts - Added a sort function to the list of notifications before they get scheduled - I chose to go with id since it's an convenient way to sort, since the id is the number of epoch seconds. the alternative is .trigger.at, but that is a full on JSDate in Luxon which is a string ("Date Thu Nov 16 2023 09:36:17 GMT-0700 (Mountain Standard Time)") and harder to sort --- www/js/splash/notifScheduler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 20f1daa66..a539ea52a 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -128,7 +128,7 @@ const scheduleNotifs = (scheme, notifTimes: DateTime[], isScheduling: boolean) = const localeCode = i18next.resolvedLanguage; const nots = notifTimes.map((n) => { const nDate = n.toJSDate(); - const seconds = nDate.getTime() / 1000; + const seconds = nDate.getTime() / 1000; // the id must be in seconds, otherwise the sorting won't work return { id: seconds, title: scheme.title[localeCode], @@ -145,6 +145,7 @@ const scheduleNotifs = (scheme, notifTimes: DateTime[], isScheduling: boolean) = // } }; }); + nots.sort((a, b) => b.id - a.id); // sort notifications by id (time) window['cordova'].plugins.notification.local.cancelAll(() => { debugGetScheduled('After cancelling'); window['cordova'].plugins.notification.local.schedule(nots, () => { From e209f1016947d189330bcd7427930049e7b7f26f Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 16 Nov 2023 13:41:03 -0700 Subject: [PATCH 21/36] Change static variable isScheduling to a useState I was not confident that isScheduling would be set properly if it were set as a static variable being passed as an arg, so I used useState and passed the value and set function --- www/js/control/ProfileSettings.jsx | 14 ++++++-- www/js/splash/notifScheduler.ts | 56 +++++++++++++++++++----------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 4cab17024..193e30b8f 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -103,7 +103,7 @@ const ProfileSettings = () => { // used for scheduling notifs let scheduledPromise = new Promise((rs) => rs()); - let isScheduling = false; + const [isScheduling, setIsScheduling] = useState(false); useEffect(() => { //added appConfig.name needed to be defined because appConfig was defined but empty @@ -152,7 +152,12 @@ const ProfileSettings = () => { } // Update the scheduled notifs - updateScheduledNotifs(tempUiConfig.reminderSchemes, isScheduling, scheduledPromise) + updateScheduledNotifs( + tempUiConfig.reminderSchemes, + isScheduling, + setIsScheduling, + scheduledPromise, + ) .then(() => { logDebug('updated scheduled notifs'); }) @@ -207,7 +212,9 @@ const ProfileSettings = () => { if (uiConfig?.reminderSchemes) { let promiseList = []; - promiseList.push(getReminderPrefs(uiConfig.reminderSchemes, isScheduling, scheduledPromise)); + promiseList.push( + getReminderPrefs(uiConfig.reminderSchemes, isScheduling, setIsScheduling, scheduledPromise), + ); promiseList.push(getScheduledNotifs(isScheduling, scheduledPromise)); let resultList = await Promise.all(promiseList); const prefs = resultList[0]; @@ -294,6 +301,7 @@ const ProfileSettings = () => { { reminder_time_of_day: m.toFormat('HH:mm') }, uiConfig.reminderSchemes, isScheduling, + setIsScheduling, scheduledPromise, ).then(() => { refreshNotificationSettings(); diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index a539ea52a..06a869876 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -122,9 +122,9 @@ const getNotifs = function () { }; // schedules the notifications using the cordova plugin -const scheduleNotifs = (scheme, notifTimes: DateTime[], isScheduling: boolean) => { +const scheduleNotifs = (scheme, notifTimes: DateTime[], setIsScheduling: Function) => { return new Promise((rs) => { - isScheduling = true; + setIsScheduling(true); const localeCode = i18next.resolvedLanguage; const nots = notifTimes.map((n) => { const nDate = n.toJSDate(); @@ -150,7 +150,7 @@ const scheduleNotifs = (scheme, notifTimes: DateTime[], isScheduling: boolean) = debugGetScheduled('After cancelling'); window['cordova'].plugins.notification.local.schedule(nots, () => { debugGetScheduled('After scheduling'); - isScheduling = false; + setIsScheduling(false); rs(); //scheduling promise resolved here }); }); @@ -163,13 +163,15 @@ const removeEmptyObjects = (list: any[]): any[] => { // determines when notifications are needed, and schedules them if not already scheduled export const updateScheduledNotifs = async ( - reminderSchemes, + reminderSchemes: object, isScheduling: boolean, + setIsScheduling: Function, scheduledPromise: Promise, ): Promise => { const { reminder_assignment, reminder_join_date, reminder_time_of_day } = await getReminderPrefs( reminderSchemes, isScheduling, + setIsScheduling, scheduledPromise, ); var scheme = {}; @@ -197,7 +199,7 @@ export const updateScheduledNotifs = async ( if (isScheduling) { console.log('ERROR: Already scheduling notifications, not scheduling again'); } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes, isScheduling); + scheduledPromise = scheduleNotifs(scheme, notifTimes, setIsScheduling); //enforcing end of scheduling to conisder update through scheduledPromise.then(() => { resolve(); @@ -246,6 +248,7 @@ interface User { export const getReminderPrefs = async ( reminderSchemes, isScheduling: boolean, + setIsScheduling: Function, scheduledPromise: Promise, ): Promise => { const userPromise = getUser(); @@ -258,32 +261,43 @@ export const getReminderPrefs = async ( console.log('User just joined, Initializing reminder prefs'); const initPrefs = initReminderPrefs(reminderSchemes); console.log('Initialized reminder prefs: ', initPrefs); - await setReminderPrefs(initPrefs, reminderSchemes, isScheduling, scheduledPromise); + await setReminderPrefs( + initPrefs, + reminderSchemes, + isScheduling, + setIsScheduling, + scheduledPromise, + ); return { ...user, ...initPrefs }; // user profile + the new prefs }; export const setReminderPrefs = async ( - newPrefs, - reminderSchemes, + newPrefs: object, + reminderSchemes: object, isScheduling: boolean, + setIsScheduling: Function, scheduledPromise: Promise, ) => { await updateUser(newPrefs); const updatePromise = new Promise((resolve, reject) => { //enforcing update before moving on - updateScheduledNotifs(reminderSchemes, isScheduling, scheduledPromise).then(() => { - resolve(); - }); + updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise).then( + () => { + resolve(); + }, + ); }); // record the new prefs in client stats - getReminderPrefs(reminderSchemes, isScheduling, scheduledPromise).then((prefs) => { - // extract only the relevant fields from the prefs, - // and add as a reading to client stats - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; - addStatReading(statKeys.REMINDER_PREFS, { - reminder_assignment, - reminder_join_date, - reminder_time_of_day, - }).then(logDebug('Added reminder prefs to client stats')); - }); + getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise).then( + (prefs) => { + // extract only the relevant fields from the prefs, + // and add as a reading to client stats + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; + addStatReading(statKeys.REMINDER_PREFS, { + reminder_assignment, + reminder_join_date, + reminder_time_of_day, + }).then(logDebug('Added reminder prefs to client stats')); + }, + ); return updatePromise; }; From b8bf1cf131d62b81c637795feb10a3311318f7fd Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 16 Nov 2023 13:42:37 -0700 Subject: [PATCH 22/36] Replace console.logs with logDebugs We want to standardize on using logDebug, so I replaced all important console.logs with logDebugs that use JSON.stringify for object printouts --- www/js/control/ProfileSettings.jsx | 22 ++++++++++++++-------- www/js/splash/notifScheduler.ts | 18 ++++++------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 193e30b8f..00b73bdf8 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -204,9 +204,9 @@ const ProfileSettings = () => { }, [editCollectionVis]); async function refreshNotificationSettings() { - console.debug( - 'about to refreshNotificationSettings, notificationSettings = ', - notificationSettings, + logDebug( + 'about to refreshNotificationSettings, notificationSettings = ' + + JSON.stringify(notificationSettings), ); const newNotificationSettings = {}; @@ -219,7 +219,12 @@ const ProfileSettings = () => { let resultList = await Promise.all(promiseList); const prefs = resultList[0]; const scheduledNotifs = resultList[1]; - console.log('prefs and scheduled notifs', resultList[0], resultList[1]); + logDebug( + 'prefs and scheduled notifs\n' + + JSON.stringify(prefs) + + '\n-\n' + + JSON.stringify(scheduledNotifs), + ); const m = DateTime.fromFormat(prefs.reminder_time_of_day, 'HH:mm'); newNotificationSettings.prefReminderTimeVal = m.toJSDate(); @@ -230,10 +235,11 @@ const ProfileSettings = () => { updatePrefReminderTime(false); } - console.log( - 'notification settings before and after', - notificationSettings, - newNotificationSettings, + logDebug( + 'notification settings before and after\n' + + JSON.stringify(notificationSettings) + + '\n-\n' + + JSON.stringify(newNotificationSettings), ); setNotificationSettings(newNotificationSettings); } diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 06a869876..79f81b3a5 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -79,10 +79,9 @@ export const getScheduledNotifs = function (isScheduling: boolean, scheduledProm if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors */ if (isScheduling) { - console.log('requesting fetch while still actively scheduling, waiting on scheduledPromise'); + logDebug('requesting fetch while still actively scheduling, waiting on scheduledPromise'); scheduledPromise.then(() => { getNotifs().then((notifs) => { - console.log('done scheduling notifs', notifs); resolve(notifs); }); }); @@ -99,7 +98,7 @@ const getNotifs = function () { return new Promise((resolve, reject) => { window['cordova'].plugins.notification.local.getScheduled((notifs) => { if (!notifs?.length) { - console.log('there are no notifications'); + logDebug('there are no notifications'); resolve([]); //if none, return empty array } else { // some empty objects slip through, remove them from notifs @@ -197,7 +196,7 @@ export const updateScheduledNotifs = async ( // we'll wait for the previous one to finish before scheduling again scheduledPromise.then(() => { if (isScheduling) { - console.log('ERROR: Already scheduling notifications, not scheduling again'); + logDebug('ERROR: Already scheduling notifications, not scheduling again'); } else { scheduledPromise = scheduleNotifs(scheme, notifTimes, setIsScheduling); //enforcing end of scheduling to conisder update through @@ -233,11 +232,6 @@ const initReminderPrefs = (reminderSchemes) => { reminder_join_date: '2023-05-09', reminder_time_of_day: '21:00', */ -// interface ReminderPrefs { -// reminder_assignment: string; -// reminder_join_date: string; -// reminder_time_of_day: string; -// } interface User { reminder_assignment: string; @@ -254,13 +248,13 @@ export const getReminderPrefs = async ( const userPromise = getUser(); const user = (await userPromise) as User; if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { - console.log('User already has reminder prefs, returning them', user); + logDebug('User already has reminder prefs, returning them: ' + JSON.stringify(user)); return user; } // if no prefs, user just joined, so initialize them - console.log('User just joined, Initializing reminder prefs'); + logDebug('User just joined, Initializing reminder prefs'); const initPrefs = initReminderPrefs(reminderSchemes); - console.log('Initialized reminder prefs: ', initPrefs); + logDebug('Initialized reminder prefs: ' + JSON.stringify(initPrefs)); await setReminderPrefs( initPrefs, reminderSchemes, From b27ae9accba2fd42b12d2f5f92355dfdc31c62f5 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 16 Nov 2023 13:46:07 -0700 Subject: [PATCH 23/36] Add in more type declarations notifScheduler.ts - notifs uses any[], because n.trigger.at is not recognized if we use object[] - TODO: We may want to create a typing file for these types, such as User, which is currently declared as an interface within this file - TODO: We will want to add a type for notifs coming from cordova, and for ReminderScheme from the config, but I'm not sure how since ReminderScheme is an object with a variety of inner objects whose keys may or may not be named the same (weekly, week-quarterly, passive, etc.) --- www/js/splash/notifScheduler.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 79f81b3a5..a92ac51c3 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -30,7 +30,7 @@ const calcNotifTimes = (scheme, dayZeroDate, timeOfDay): DateTime[] => { }; // returns true if all expected times are already scheduled -const areAlreadyScheduled = (notifs, expectedTimes) => { +const areAlreadyScheduled = (notifs: any[], expectedTimes: DateTime[]) => { for (const t of expectedTimes) { if (!notifs.some((n) => DateTime.fromMillis(n.trigger.at).equals(t))) { return false; @@ -81,12 +81,12 @@ export const getScheduledNotifs = function (isScheduling: boolean, scheduledProm if (isScheduling) { logDebug('requesting fetch while still actively scheduling, waiting on scheduledPromise'); scheduledPromise.then(() => { - getNotifs().then((notifs) => { + getNotifs().then((notifs: object[]) => { resolve(notifs); }); }); } else { - getNotifs().then((notifs) => { + getNotifs().then((notifs: object[]) => { resolve(notifs); }); } @@ -96,7 +96,7 @@ export const getScheduledNotifs = function (isScheduling: boolean, scheduledProm //get scheduled notifications from cordova plugin and format them const getNotifs = function () { return new Promise((resolve, reject) => { - window['cordova'].plugins.notification.local.getScheduled((notifs) => { + window['cordova'].plugins.notification.local.getScheduled((notifs: any[]) => { if (!notifs?.length) { logDebug('there are no notifications'); resolve([]); //if none, return empty array @@ -108,8 +108,8 @@ const getNotifs = function () { const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing let scheduledNotifs = []; scheduledNotifs = notifSubset.map((n) => { - const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); - const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + const time: string = DateTime.fromMillis(n.trigger.at).toFormat('t'); + const date: string = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); return { key: date, val: time, @@ -184,9 +184,9 @@ export const updateScheduledNotifs = async ( reminder_assignment, ); } - const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day) as DateTime[]; + const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); return new Promise((resolve, reject) => { - window['cordova'].plugins.notification.local.getScheduled((notifs) => { + window['cordova'].plugins.notification.local.getScheduled((notifs: any[]) => { // some empty objects slip through, remove them from notifs notifs = removeEmptyObjects(notifs); if (areAlreadyScheduled(notifs, notifTimes)) { @@ -214,12 +214,12 @@ export const updateScheduledNotifs = async ( and use the default time of day from config (or noon if not specified) This is only called once when the user first joins the study */ -const initReminderPrefs = (reminderSchemes) => { +const initReminderPrefs = (reminderSchemes: object): object => { // randomly assign from the schemes listed in config const schemes = Object.keys(reminderSchemes); - const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; - const todayDate = DateTime.local().toFormat('yyyy-MM-dd'); - const defaultTime = reminderSchemes[randAssignment]?.defaultTime || '12:00'; + const randAssignment: string = schemes[Math.floor(Math.random() * schemes.length)]; + const todayDate: string = DateTime.local().toFormat('yyyy-MM-dd'); + const defaultTime: string = reminderSchemes[randAssignment]?.defaultTime || '12:00'; return { reminder_assignment: randAssignment, reminder_join_date: todayDate, @@ -240,7 +240,7 @@ interface User { } export const getReminderPrefs = async ( - reminderSchemes, + reminderSchemes: object, isScheduling: boolean, setIsScheduling: Function, scheduledPromise: Promise, @@ -270,7 +270,7 @@ export const setReminderPrefs = async ( isScheduling: boolean, setIsScheduling: Function, scheduledPromise: Promise, -) => { +): Promise => { await updateUser(newPrefs); const updatePromise = new Promise((resolve, reject) => { //enforcing update before moving on From abbe618cb816bf6f5d3d4fdaaadd7536281e04c2 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 16 Nov 2023 15:43:09 -0700 Subject: [PATCH 24/36] Add Jest Testing - getScheduledNotifs cordovaMocks.ts - Added a reminder mock for the cordova plugin for reminders notifScheduler.test.ts - Mock the cordova plugin to return the active reminders - Test the getScheduledNotifs function --- www/__mocks__/cordovaMocks.ts | 8 ++ www/__tests__/notifScheduler.test.ts | 115 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 www/__tests__/notifScheduler.test.ts diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 62aa9be1a..8b7322204 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -7,6 +7,14 @@ export const mockCordova = () => { window['cordova'].plugins ||= {}; }; +export const mockReminders = () => { + window['cordova'] ||= {}; + window['cordova'].plugins ||= {}; + window['cordova'].plugins.notification ||= {}; + window['cordova'].plugins.notification.local ||= {}; + window['cordova'].plugins.notification.local.getScheduled ||= () => []; +}; + export const mockDevice = () => { window['device'] ||= {}; window['device'].platform ||= 'ios'; diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts new file mode 100644 index 000000000..de4ce27d7 --- /dev/null +++ b/www/__tests__/notifScheduler.test.ts @@ -0,0 +1,115 @@ +import { mockReminders } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import { DateTime } from 'luxon'; +import { + getScheduledNotifs, + updateScheduledNotifs, + getReminderPrefs, + setReminderPrefs, +} from '../js/splash/notifScheduler'; + +mockLogger(); +mockReminders(); + +jest.mock('../js/splash/notifScheduler', () => ({ + ...jest.requireActual('../js/splash/notifScheduler'), + getNotifs: jest.fn(), +})); + +describe('getScheduledNotifs', () => { + it('should resolve with notifications while not actively scheduling', async () => { + const isScheduling = false; + const scheduledPromise = Promise.resolve(); + const mockNotifications = [{ trigger: { at: DateTime.now().toMillis() } }]; + const expectedResult = [ + { + key: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('t'), + }, + ]; + + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementation((callback) => callback(mockNotifications)); + const scheduledNotifs = await getScheduledNotifs(isScheduling, Promise.resolve()); + + expect(scheduledNotifs).toEqual(expectedResult); + }); + + it('should resolve with notifications if actively scheduling', async () => { + const isScheduling = true; + const scheduledPromise = Promise.resolve(); + const mockNotifications = [{ trigger: { at: DateTime.now().toMillis() } }]; + const expectedResult = [ + { + key: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('t'), + }, + ]; + + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementation((callback) => callback(mockNotifications)); + const scheduledNotifs = await getScheduledNotifs(isScheduling, scheduledPromise); + + expect(scheduledNotifs).toEqual(expectedResult); + }); + + it('should handle case where no notifications are present', async () => { + const isScheduling = false; + const scheduledPromise = Promise.resolve(); + const mockNotifications = []; + const expectedResult = []; + + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementation((callback) => callback(mockNotifications)); + const scheduledNotifs = await getScheduledNotifs(isScheduling, Promise.resolve()); + + expect(scheduledNotifs).toEqual(expectedResult); + }); + + it('should handle the case where greater than 5 notifications are present', async () => { + const isScheduling = false; + const scheduledPromise = Promise.resolve(); + const mockNotifications = [ + { trigger: { at: DateTime.now().toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 1 }).toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 2 }).toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 3 }).toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 4 }).toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 5 }).toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 6 }).toMillis() } }, + { trigger: { at: DateTime.now().plus({ weeks: 7 }).toMillis() } }, + ]; + const expectedResult = [ + { + key: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('t'), + }, + { + key: DateTime.fromMillis(mockNotifications[1].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[1].trigger.at).toFormat('t'), + }, + { + key: DateTime.fromMillis(mockNotifications[2].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[2].trigger.at).toFormat('t'), + }, + { + key: DateTime.fromMillis(mockNotifications[3].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[3].trigger.at).toFormat('t'), + }, + { + key: DateTime.fromMillis(mockNotifications[4].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifications[4].trigger.at).toFormat('t'), + }, + ]; + + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementation((callback) => callback(mockNotifications)); + const scheduledNotifs = await getScheduledNotifs(isScheduling, Promise.resolve()); + + expect(scheduledNotifs).toEqual(expectedResult); + }); +}); From fbd1b626413ffc60082490c4bb09a5fda660adbd Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 16 Nov 2023 17:04:10 -0700 Subject: [PATCH 25/36] Add first updateScheduledNotifs test A lot of mocking was required to do this: globalMocks.ts - Added log mock for console.alert - Added log mock for console.error - Both of these are called in displayErrorMsg, which is why we have to mock them notifScheduler.ts - Added a resolve in the if statement for the case of "reminders are already scheduled", so that we don't keep running the test and possibly time-out notifScheduler.test.ts - added new mocks for clientStats, logger, commHelper, and notifScheduler - Added comments for clarity in tests - Renamed any instances of mockNotifications to mockNotifs for more clarity and to mirror the variable name notifs in the actual file - Created the new test for updateScheduledNotifs that currently only tests to see if it sees that notifications have already been scheduled and then ends --- www/__mocks__/globalMocks.ts | 6 + www/__tests__/notifScheduler.test.ts | 177 +++++++++++++++++++++++---- www/js/splash/notifScheduler.ts | 1 + 3 files changed, 162 insertions(+), 22 deletions(-) diff --git a/www/__mocks__/globalMocks.ts b/www/__mocks__/globalMocks.ts index f13cb274b..0e518897c 100644 --- a/www/__mocks__/globalMocks.ts +++ b/www/__mocks__/globalMocks.ts @@ -1,3 +1,9 @@ export const mockLogger = () => { window['Logger'] = { log: console.log }; + window.alert = (msg) => { + console.log(msg); + }; + console.error = (msg) => { + console.log(msg); + }; }; diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index de4ce27d7..00a925678 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -1,5 +1,6 @@ import { mockReminders } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; +import { logDebug } from '../js/plugin/logger'; import { DateTime } from 'luxon'; import { getScheduledNotifs, @@ -8,71 +9,169 @@ import { setReminderPrefs, } from '../js/splash/notifScheduler'; +const exampleReminderSchemes = { + weekly: { + title: { + en: 'Please take a moment to label your trips', + es: 'Por favor, tómese un momento para etiquetar sus viajes', + }, + text: { + en: 'Click to open the app and view unlabeled trips', + es: 'Haga clic para abrir la aplicación y ver los viajes sin etiquetar', + }, + schedule: [ + { start: 0, end: 1, intervalInDays: 1 }, + { start: 3, end: 5, intervalInDays: 2 }, + ], + defaultTime: '21:00', + }, + 'week-quarterly': { + title: { + en: 'Please take a moment to label your trips', + es: 'Por favor, tómese un momento para etiquetar sus viajes', + }, + text: { + en: 'Click to open the app and view unlabeled trips', + es: 'Haga clic para abrir la aplicación y ver los viajes sin etiquetar', + }, + schedule: [ + { start: 0, end: 1, intervalInDays: 1 }, + { start: 3, end: 5, intervalInDays: 2 }, + ], + defaultTime: '22:00', + }, + passive: { + title: { + en: 'Please take a moment to label your trips', + es: 'Por favor, tómese un momento para etiquetar sus viajes', + }, + text: { + en: 'Click to open the app and view unlabeled trips', + es: 'Haga clic para abrir la aplicación y ver los viajes sin etiquetar', + }, + schedule: [ + { start: 0, end: 1, intervalInDays: 1 }, + { start: 3, end: 5, intervalInDays: 2 }, + ], + defaultTime: '23:00', + }, +}; + mockLogger(); mockReminders(); +jest.mock('../js/commHelper', () => ({ + ...jest.requireActual('../js/commHelper'), + getUser: jest.fn(() => + Promise.resolve({ + // These values are **important**... + // reminder_assignment: must match a key from the reminder scheme above, + // reminder_join_date: must match the first day of the mocked notifs below in the tests, + // reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }), + ), + updateUser: jest.fn(() => Promise.resolve()), +})); + +jest.mock('../js/plugin/clientStats', () => ({ + ...jest.requireActual('../js/plugin/clientStats'), + addStatReading: jest.fn(), +})); + +jest.mock('../js/plugin/logger', () => ({ + ...jest.requireActual('../js/plugin/logger'), + logDebug: jest.fn(), +})); + jest.mock('../js/splash/notifScheduler', () => ({ ...jest.requireActual('../js/splash/notifScheduler'), + // for getScheduledNotifs getNotifs: jest.fn(), + // for updateScheduledNotifs + getReminderPrefs: jest.fn(), + calcNotifTimes: jest.fn(), + removeEmptyObjects: jest.fn(), + areAlreadyScheduled: jest.fn(), + scheduleNotifs: jest.fn(), })); describe('getScheduledNotifs', () => { it('should resolve with notifications while not actively scheduling', async () => { + // getScheduledNotifs arguments const isScheduling = false; const scheduledPromise = Promise.resolve(); - const mockNotifications = [{ trigger: { at: DateTime.now().toMillis() } }]; + // create the mock notifs from cordova plugin + const mockNotifs = [{ trigger: { at: DateTime.now().toMillis() } }]; + // create the expected result const expectedResult = [ { - key: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('t'), }, ]; + // mock the cordova plugin jest .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') - .mockImplementation((callback) => callback(mockNotifications)); + .mockImplementation((callback) => callback(mockNotifs)); + // call the function const scheduledNotifs = await getScheduledNotifs(isScheduling, Promise.resolve()); expect(scheduledNotifs).toEqual(expectedResult); }); it('should resolve with notifications if actively scheduling', async () => { + // getScheduledNotifs arguments const isScheduling = true; const scheduledPromise = Promise.resolve(); - const mockNotifications = [{ trigger: { at: DateTime.now().toMillis() } }]; + // create the mock notifs from cordova plugin + const mockNotifs = [{ trigger: { at: DateTime.now().toMillis() } }]; + // create the expected result const expectedResult = [ { - key: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('t'), }, ]; + // mock the cordova plugin jest .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') - .mockImplementation((callback) => callback(mockNotifications)); + .mockImplementation((callback) => callback(mockNotifs)); + // call the funciton const scheduledNotifs = await getScheduledNotifs(isScheduling, scheduledPromise); expect(scheduledNotifs).toEqual(expectedResult); }); it('should handle case where no notifications are present', async () => { + // getScheduledNotifs arguments const isScheduling = false; const scheduledPromise = Promise.resolve(); - const mockNotifications = []; + // create the mock notifs from cordova plugin + const mockNotifs = []; + // create the expected result const expectedResult = []; + // mock the cordova plugin jest .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') - .mockImplementation((callback) => callback(mockNotifications)); + .mockImplementation((callback) => callback(mockNotifs)); + // call the funciton const scheduledNotifs = await getScheduledNotifs(isScheduling, Promise.resolve()); expect(scheduledNotifs).toEqual(expectedResult); }); it('should handle the case where greater than 5 notifications are present', async () => { + // getScheduledNotifs arguments const isScheduling = false; const scheduledPromise = Promise.resolve(); - const mockNotifications = [ + // create the mock notifs from cordova plugin (greater than 5 notifications) + const mockNotifs = [ { trigger: { at: DateTime.now().toMillis() } }, { trigger: { at: DateTime.now().plus({ weeks: 1 }).toMillis() } }, { trigger: { at: DateTime.now().plus({ weeks: 2 }).toMillis() } }, @@ -82,34 +181,68 @@ describe('getScheduledNotifs', () => { { trigger: { at: DateTime.now().plus({ weeks: 6 }).toMillis() } }, { trigger: { at: DateTime.now().plus({ weeks: 7 }).toMillis() } }, ]; + // create the expected result (only the first 5 notifications) const expectedResult = [ { - key: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[0].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifications[1].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[1].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[1].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[1].trigger.at).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifications[2].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[2].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[2].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[2].trigger.at).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifications[3].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[3].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[3].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[3].trigger.at).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifications[4].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifications[4].trigger.at).toFormat('t'), + key: DateTime.fromMillis(mockNotifs[4].trigger.at).toFormat('DDD'), + val: DateTime.fromMillis(mockNotifs[4].trigger.at).toFormat('t'), }, ]; + // mock the cordova plugin jest .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') - .mockImplementation((callback) => callback(mockNotifications)); + .mockImplementation((callback) => callback(mockNotifs)); + // call the funciton const scheduledNotifs = await getScheduledNotifs(isScheduling, Promise.resolve()); expect(scheduledNotifs).toEqual(expectedResult); }); }); + +describe('updateScheduledNotifs', () => { + afterEach(() => { + jest.restoreAllMocks(); // Restore mocked functions after each test + }); + + it('should resolve without scheduling if notifications are already scheduled', async () => { + // updateScheduleNotifs arguments + const reminderSchemes: any = exampleReminderSchemes; + let isScheduling: boolean = false; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + // create the mock notifs from cordova plugin (must match the notifs that will generate from the reminder scheme above... + // in this case: exampleReminderSchemes.weekly, because getUser is mocked to return reminder_assignment: 'weekly') + const mockNotifs = [ + { trigger: { at: DateTime.fromFormat('2023-11-14 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, + { trigger: { at: DateTime.fromFormat('2023-11-15 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, + { trigger: { at: DateTime.fromFormat('2023-11-17 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, + { trigger: { at: DateTime.fromFormat('2023-11-19 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, + ]; + + // mock the cordova plugin + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementationOnce((callback) => callback(mockNotifs)); + // call the function + await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + + expect(logDebug).toHaveBeenCalledWith('Already scheduled, not scheduling again'); + }); +}); diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index a92ac51c3..55c4b3cdb 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -191,6 +191,7 @@ export const updateScheduledNotifs = async ( notifs = removeEmptyObjects(notifs); if (areAlreadyScheduled(notifs, notifTimes)) { logDebug('Already scheduled, not scheduling again'); + resolve(); } else { // to ensure we don't overlap with the last scheduling() request, // we'll wait for the previous one to finish before scheduling again From 87fdae505b80ac0d0535ebeddecbb8b5ffd4a48f Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 21 Nov 2023 09:33:48 -0600 Subject: [PATCH 26/36] Add reminder scheme missing test for updateScheduledNotifs I also had to adjust the error coming from notifScheduler because it wasn't helping much with the way it was defined before --- www/__tests__/notifScheduler.test.ts | 15 +++++++++++++++ www/js/splash/notifScheduler.ts | 14 ++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index 00a925678..f49cd84bc 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -245,4 +245,19 @@ describe('updateScheduledNotifs', () => { expect(logDebug).toHaveBeenCalledWith('Already scheduled, not scheduling again'); }); + + it('should log an error message if the reminder scheme is missing', async () => { + // updateScheduleNotifs arguments + let reminderSchemes: any = exampleReminderSchemes; + delete reminderSchemes.weekly; // delete the weekly reminder scheme, to create a missing reminder scheme error + let isScheduling: boolean = false; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + + await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + + // Your assertions here + expect(logDebug).toHaveBeenCalledWith('Error: Reminder scheme not found'); + // Add more assertions as needed + }); }); diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 55c4b3cdb..114d5d9b9 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -173,16 +173,10 @@ export const updateScheduledNotifs = async ( setIsScheduling, scheduledPromise, ); - var scheme = {}; - try { - scheme = reminderSchemes[reminder_assignment]; - } catch (e) { - displayErrorMsg( - 'ERROR: Could not find reminder scheme for assignment ' + - reminderSchemes + - ' - ' + - reminder_assignment, - ); + const scheme = reminderSchemes[reminder_assignment]; + if (scheme === undefined) { + logDebug('Error: Reminder scheme not found'); + return; } const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); return new Promise((resolve, reject) => { From 214081ea23f499cbf214b55bea823b0294b137c4 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Wed, 22 Nov 2023 13:37:46 -0600 Subject: [PATCH 27/36] Add already scheduled notifs test --- www/__tests__/notifScheduler.test.ts | 23 ++++++++++++++++++++++- www/js/splash/notifScheduler.ts | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index f49cd84bc..0728ebe8f 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -239,13 +239,34 @@ describe('updateScheduledNotifs', () => { // mock the cordova plugin jest .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') - .mockImplementationOnce((callback) => callback(mockNotifs)); + .mockImplementation((callback) => callback(mockNotifs)); // call the function await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); expect(logDebug).toHaveBeenCalledWith('Already scheduled, not scheduling again'); }); + it('should wait for the previous scheduling to finish if isScheduling is true', async () => { + // updateScheduleNotifs arguments + const reminderSchemes: any = exampleReminderSchemes; + let isScheduling: boolean = true; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + // create an empty array of mock notifs from cordova plugin + const mockNotifs = []; + + // mock the cordova plugin + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementation((callback) => callback(mockNotifs)); + // call the function + await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + + expect(logDebug).toHaveBeenCalledWith( + 'ERROR: Already scheduling notifications, not scheduling again', + ); + }); + it('should log an error message if the reminder scheme is missing', async () => { // updateScheduleNotifs arguments let reminderSchemes: any = exampleReminderSchemes; diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 114d5d9b9..d9522d92a 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -192,6 +192,7 @@ export const updateScheduledNotifs = async ( scheduledPromise.then(() => { if (isScheduling) { logDebug('ERROR: Already scheduling notifications, not scheduling again'); + resolve(); } else { scheduledPromise = scheduleNotifs(scheme, notifTimes, setIsScheduling); //enforcing end of scheduling to conisder update through From f5ebb1efe980b121c325b9903ecb3fe68bb83c6b Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Wed, 22 Nov 2023 13:39:53 -0600 Subject: [PATCH 28/36] Fixing comments --- www/__tests__/notifScheduler.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index 0728ebe8f..9dbc5e8fa 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -274,11 +274,9 @@ describe('updateScheduledNotifs', () => { let isScheduling: boolean = false; const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); const scheduledPromise: Promise = Promise.resolve(); - + // call the function await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); - // Your assertions here expect(logDebug).toHaveBeenCalledWith('Error: Reminder scheme not found'); - // Add more assertions as needed }); }); From 6892580286b8c8ae627222a11e73b55c87a95562 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 24 Nov 2023 17:19:05 -0600 Subject: [PATCH 29/36] Final overall test for updateScheduledNotifs This tests the final functionality and longest path through this function --- www/__mocks__/cordovaMocks.ts | 2 ++ www/__tests__/notifScheduler.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 8b7322204..ed58b9c18 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -13,6 +13,8 @@ export const mockReminders = () => { window['cordova'].plugins.notification ||= {}; window['cordova'].plugins.notification.local ||= {}; window['cordova'].plugins.notification.local.getScheduled ||= () => []; + window['cordova'].plugins.notification.local.cancelAll ||= () => {}; + window['cordova'].plugins.notification.local.schedule ||= () => {}; }; export const mockDevice = () => { diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index 9dbc5e8fa..a8543e3f0 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -221,6 +221,34 @@ describe('updateScheduledNotifs', () => { jest.restoreAllMocks(); // Restore mocked functions after each test }); + it('should resolve after scheduling notifications', async () => { + // updateScheduleNotifs arguments + const reminderSchemes: any = exampleReminderSchemes; + let isScheduling: boolean = false; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + // create an empty array of mock notifs from cordova plugin + const mockNotifs = []; + + // mock the cordova plugin + jest + .spyOn(window['cordova'].plugins.notification.local, 'getScheduled') + .mockImplementation((callback) => callback(mockNotifs)); + jest + .spyOn(window['cordova'].plugins.notification.local, 'cancelAll') + .mockImplementation((callback) => callback()); + jest + .spyOn(window['cordova'].plugins.notification.local, 'schedule') + .mockImplementation((arg, callback) => callback(arg)); + // call the function + await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + + expect(setIsScheduling).toHaveBeenCalledWith(true); + expect(logDebug).toHaveBeenCalledWith('After cancelling, there are no scheduled notifications'); + expect(logDebug).toHaveBeenCalledWith('After scheduling, there are no scheduled notifications'); + expect(setIsScheduling).toHaveBeenCalledWith(false); + }); + it('should resolve without scheduling if notifications are already scheduled', async () => { // updateScheduleNotifs arguments const reminderSchemes: any = exampleReminderSchemes; From 85f0519cc22960eb48bab0684bcdd257de7298a9 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 30 Nov 2023 15:58:04 -0700 Subject: [PATCH 30/36] Added user exists test for getReminderPrefs --- www/__tests__/notifScheduler.test.ts | 25 ++++++++++++++++++++++++- www/js/splash/notifScheduler.ts | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index a8543e3f0..be7886639 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -91,7 +91,6 @@ jest.mock('../js/splash/notifScheduler', () => ({ // for getScheduledNotifs getNotifs: jest.fn(), // for updateScheduledNotifs - getReminderPrefs: jest.fn(), calcNotifTimes: jest.fn(), removeEmptyObjects: jest.fn(), areAlreadyScheduled: jest.fn(), @@ -308,3 +307,27 @@ describe('updateScheduledNotifs', () => { expect(logDebug).toHaveBeenCalledWith('Error: Reminder scheme not found'); }); }); + +describe('getReminderPrefs', () => { + it('should resolve with reminder prefs when user exists', async () => { + // getReminderPrefs arguments + const reminderSchemes: any = exampleReminderSchemes; + let isScheduling: boolean = true; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + // create the expected result + const expectedResult = { + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }; + + // call the function + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = + await getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + + expect(reminder_assignment).toEqual(expectedResult.reminder_assignment); + expect(reminder_join_date).toEqual(expectedResult.reminder_join_date); + expect(reminder_time_of_day).toEqual(expectedResult.reminder_time_of_day); + }); +}); diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index d9522d92a..3997e5a03 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -243,6 +243,7 @@ export const getReminderPrefs = async ( ): Promise => { const userPromise = getUser(); const user = (await userPromise) as User; + console.log('user', user); if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { logDebug('User already has reminder prefs, returning them: ' + JSON.stringify(user)); return user; From 0528fb0186e4f34a779fc8004bf333a557de8014 Mon Sep 17 00:00:00 2001 From: Sebastian Barry <61334340+sebastianbarry@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:05:06 -0700 Subject: [PATCH 31/36] Remove unnecessary console.log --- www/js/splash/notifScheduler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 3997e5a03..d9522d92a 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -243,7 +243,6 @@ export const getReminderPrefs = async ( ): Promise => { const userPromise = getUser(); const user = (await userPromise) as User; - console.log('user', user); if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { logDebug('User already has reminder prefs, returning them: ' + JSON.stringify(user)); return user; From 2c0ec40fe70da2a44669df9a6b369ee0d3e51a3d Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 30 Nov 2023 16:29:35 -0700 Subject: [PATCH 32/36] Fixing moved location of commHelper causing jest tests to fail --- www/__tests__/notifScheduler.test.ts | 4 ++-- www/js/splash/notifScheduler.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index be7886639..e5ad213ee 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -60,8 +60,8 @@ const exampleReminderSchemes = { mockLogger(); mockReminders(); -jest.mock('../js/commHelper', () => ({ - ...jest.requireActual('../js/commHelper'), +jest.mock('../js/services/commHelper', () => ({ + ...jest.requireActual('../js/services/commHelper'), getUser: jest.fn(() => Promise.resolve({ // These values are **important**... diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index d9522d92a..cb511bea8 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -1,5 +1,5 @@ import { addStatReading, statKeys } from '../plugin/clientStats'; -import { getUser, updateUser } from '../commHelper'; +import { getUser, updateUser } from '../services/commHelper'; import { displayErrorMsg, logDebug } from '../plugin/logger'; import { DateTime } from 'luxon'; import i18next from 'i18next'; From 4e44dba7aa62107cd1e0053fbfabb3a31dac94f8 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 6 Dec 2023 14:54:08 -0500 Subject: [PATCH 33/36] fix error on configs without reminderSchemes If there are no reminderSchemes in the appConfig, notifScheduler doesn't need to do anything so let's not invoke it. Also, let's create the type definition for reminderSchemes and use it here in notifScheduler. --- www/js/control/ProfileSettings.jsx | 28 +++++++++++++++------------- www/js/splash/notifScheduler.ts | 7 ++++--- www/js/types/appConfigTypes.ts | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 9f8dd19bb..36102a252 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -143,19 +143,21 @@ const ProfileSettings = () => { tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; } - // Update the scheduled notifs - updateScheduledNotifs( - tempUiConfig.reminderSchemes, - isScheduling, - setIsScheduling, - scheduledPromise, - ) - .then(() => { - logDebug('updated scheduled notifs'); - }) - .catch((err) => { - displayErrorMsg('Error while updating scheduled notifs', err); - }); + if (tempUiConfig.reminderSchemes) { + // Update the scheduled notifs + updateScheduledNotifs( + tempUiConfig.reminderSchemes, + isScheduling, + setIsScheduling, + scheduledPromise, + ) + .then(() => { + logDebug('updated scheduled notifs'); + }) + .catch((err) => { + displayErrorMsg('Error while updating scheduled notifs', err); + }); + } // setTemplateText(tempUiConfig.intro.translated_text); // console.log("translated text is??", templateText); diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index cb511bea8..0111dba26 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -3,6 +3,7 @@ import { getUser, updateUser } from '../services/commHelper'; import { displayErrorMsg, logDebug } from '../plugin/logger'; import { DateTime } from 'luxon'; import i18next from 'i18next'; +import { ReminderSchemeConfig } from '../types/appConfigTypes'; // like python range() function range(start, stop, step) { @@ -162,7 +163,7 @@ const removeEmptyObjects = (list: any[]): any[] => { // determines when notifications are needed, and schedules them if not already scheduled export const updateScheduledNotifs = async ( - reminderSchemes: object, + reminderSchemes: ReminderSchemeConfig, isScheduling: boolean, setIsScheduling: Function, scheduledPromise: Promise, @@ -236,7 +237,7 @@ interface User { } export const getReminderPrefs = async ( - reminderSchemes: object, + reminderSchemes: ReminderSchemeConfig, isScheduling: boolean, setIsScheduling: Function, scheduledPromise: Promise, @@ -262,7 +263,7 @@ export const getReminderPrefs = async ( }; export const setReminderPrefs = async ( newPrefs: object, - reminderSchemes: object, + reminderSchemes: ReminderSchemeConfig, isScheduling: boolean, setIsScheduling: Function, scheduledPromise: Promise, diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index 1a2e50722..a737ae2f3 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -7,6 +7,7 @@ export type AppConfig = { 'trip-labels': 'MULTILABEL' | 'ENKETO'; surveys: EnketoSurveyConfig; }; + reminderSchemes?: ReminderSchemeConfig; [k: string]: any; // TODO fill in all the other fields }; @@ -25,3 +26,16 @@ export type EnketoSurveyConfig = { dataKey?: string; }; }; + +export type ReminderSchemeConfig = { + [schemeName: string]: { + title: { [lang: string]: string }; + message: { [lang: string]: string }; + schedule: { + start: number; + end: number; + intervalInDays: number; + }[]; + defaultTime: string; // format is HH:MM in 24 hour time + }; +}; From 5ee4d6430caa1b9d101b0762acc8c84637da1cc6 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 12 Dec 2023 10:59:20 -0700 Subject: [PATCH 34/36] Add getReminderPrefs test for when user does not exist notifScheduler.test.ts - Imported getUser and updateUser so that we can mock them in the tests - Imported addStatReading so we can mock it to do nothing - Removed the old way of mocking the implementation of getUser, and the new way allows us to change them per-test - Added automatic mock for clientStats - added beforeEach statements to mock getUser inside the describe blocks - --- www/__tests__/notifScheduler.test.ts | 96 ++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index e5ad213ee..e8dfde6ef 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -2,6 +2,8 @@ import { mockReminders } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import { logDebug } from '../js/plugin/logger'; import { DateTime } from 'luxon'; +import { getUser, updateUser } from '../js/services/commHelper'; +import { addStatReading } from '../js/plugin/clientStats'; import { getScheduledNotifs, updateScheduledNotifs, @@ -61,20 +63,11 @@ mockLogger(); mockReminders(); jest.mock('../js/services/commHelper', () => ({ - ...jest.requireActual('../js/services/commHelper'), - getUser: jest.fn(() => - Promise.resolve({ - // These values are **important**... - // reminder_assignment: must match a key from the reminder scheme above, - // reminder_join_date: must match the first day of the mocked notifs below in the tests, - // reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above - reminder_assignment: 'weekly', - reminder_join_date: '2023-11-14', - reminder_time_of_day: '21:00', - }), - ), - updateUser: jest.fn(() => Promise.resolve()), + getUser: jest.fn(), + updateUser: jest.fn(), })); +const mockGetUser = getUser as jest.Mock; +const mockUpdateUser = updateUser as jest.Mock; jest.mock('../js/plugin/clientStats', () => ({ ...jest.requireActual('../js/plugin/clientStats'), @@ -97,6 +90,8 @@ jest.mock('../js/splash/notifScheduler', () => ({ scheduleNotifs: jest.fn(), })); +jest.mock('../js/plugin/clientStats'); + describe('getScheduledNotifs', () => { it('should resolve with notifications while not actively scheduling', async () => { // getScheduledNotifs arguments @@ -220,6 +215,21 @@ describe('updateScheduledNotifs', () => { jest.restoreAllMocks(); // Restore mocked functions after each test }); + beforeEach(() => { + // mock the getUser function + mockGetUser.mockImplementation(() => + Promise.resolve({ + // These values are **important**... + // reminder_assignment: must match a key from the reminder scheme above, + // reminder_join_date: must match the first day of the mocked notifs below in the tests, + // reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }), + ); + }); + it('should resolve after scheduling notifications', async () => { // updateScheduleNotifs arguments const reminderSchemes: any = exampleReminderSchemes; @@ -309,6 +319,53 @@ describe('updateScheduledNotifs', () => { }); describe('getReminderPrefs', () => { + afterEach(() => { + jest.restoreAllMocks(); // Restore mocked functions after each test + }); + + it('should resolve with newly initialilzed reminder prefs when user does not exist', async () => { + // getReminderPrefs arguments + const reminderSchemes: any = exampleReminderSchemes; + let isScheduling: boolean = true; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + // create the expected result + const expectedResult = { + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }; + + // mock the getUser function to return a user that does not exist: + // first, as undefined to get the not-yet-created user behavior, + // then, as a user with reminder prefs to prevent infinite looping (since updateUser does not update the user) + mockGetUser + .mockImplementation(() => + Promise.resolve({ + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }), + ) + .mockImplementationOnce(() => + Promise.resolve({ + reminder_assignment: undefined, + reminder_join_date: undefined, + reminder_time_of_day: undefined, + }), + ); + // mock addStatReading for the setReminderPrefs portion of getReminderPrefs + // typescript causes us to need to use "... as jest.Mock" to mock funcitons that are imported from other files + (addStatReading as jest.Mock).mockImplementation(() => Promise.resolve()); + + // call the function + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = + await getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + + expect(logDebug).toHaveBeenCalledWith('User just joined, Initializing reminder prefs'); + expect(logDebug).toHaveBeenCalledWith('Added reminder prefs to client stats'); + }); + it('should resolve with reminder prefs when user exists', async () => { // getReminderPrefs arguments const reminderSchemes: any = exampleReminderSchemes; @@ -322,6 +379,19 @@ describe('getReminderPrefs', () => { reminder_time_of_day: '21:00', }; + // mock the getUser function + mockGetUser.mockImplementation(() => + Promise.resolve({ + // These values are **important**... + // reminder_assignment: must match a key from the reminder scheme above, + // reminder_join_date: must match the first day of the mocked notifs below in the tests, + // reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }), + ); + // call the function const { reminder_assignment, reminder_join_date, reminder_time_of_day } = await getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); From d3750c344c23143738869d2ea7d5dca219be0efa Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 12 Dec 2023 11:24:49 -0700 Subject: [PATCH 35/36] Add simple setReminderPrefs test - Tests to see if setReminderPrefs works when called in the way that ProfileSettings.jsx calls it --- www/__tests__/notifScheduler.test.ts | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index e8dfde6ef..fca4a03d4 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -401,3 +401,53 @@ describe('getReminderPrefs', () => { expect(reminder_time_of_day).toEqual(expectedResult.reminder_time_of_day); }); }); + +describe('setReminderPrefs', () => { + afterEach(() => { + jest.restoreAllMocks(); // Restore mocked functions after each test + }); + + beforeEach(() => { + // mock the getUser function + mockGetUser.mockImplementation(() => + Promise.resolve({ + // These values are **important**... + // reminder_assignment: must match a key from the reminder scheme above, + // reminder_join_date: must match the first day of the mocked notifs below in the tests, + // reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above + reminder_assignment: 'weekly', + reminder_join_date: '2023-11-14', + reminder_time_of_day: '21:00', + }), + ); + }); + + it('should resolve with promise that calls updateScheduledNotifs', async () => { + // setReminderPrefs arguments + const newPrefs: any = { + reminder_time_of_day: '21:00', + }; + const reminderSchemes: any = exampleReminderSchemes; + let isScheduling: boolean = true; + const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); + const scheduledPromise: Promise = Promise.resolve(); + + // mock the updateUser function + mockUpdateUser.mockImplementation(() => Promise.resolve()); + + // call the function + setReminderPrefs( + newPrefs, + reminderSchemes, + isScheduling, + setIsScheduling, + scheduledPromise, + ).then(() => { + // in the implementation in ProfileSettings.jsx, + // refresNotificationSettings(); + // would be called next + }); + + expect(logDebug).toBeCalledWith('Added reminder prefs to client stats'); + }); +}); From ec9729de081000fcdc9bed288b8da5c0ee3b77c8 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 14 Dec 2023 17:02:58 -0700 Subject: [PATCH 36/36] Add test for updateScheduledNotifs to test if notifs successfully get scheduled - There were many places where I used to/fromMillis instead of to/fromJSDate, causing a type difference that was causing errors - Added a i18n mock because not having a language code return was messing with the debugScheduledNotifs log outputs - Added a funcitonality for mockNotifs in the test, so that it can be adjusted, cleared, added to, etc. so that we can test with getScheduledNotifs if it successfully scheduled the notifs --- www/__tests__/notifScheduler.test.ts | 82 +++++++++++++++++----------- www/js/splash/notifScheduler.ts | 12 ++-- 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/www/__tests__/notifScheduler.test.ts b/www/__tests__/notifScheduler.test.ts index fca4a03d4..8b74fe7ba 100644 --- a/www/__tests__/notifScheduler.test.ts +++ b/www/__tests__/notifScheduler.test.ts @@ -1,5 +1,6 @@ import { mockReminders } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; +import i18next from 'i18next'; import { logDebug } from '../js/plugin/logger'; import { DateTime } from 'luxon'; import { getUser, updateUser } from '../js/services/commHelper'; @@ -62,6 +63,10 @@ const exampleReminderSchemes = { mockLogger(); mockReminders(); +jest.mock('i18next', () => ({ + resolvedLanguage: 'en', +})); + jest.mock('../js/services/commHelper', () => ({ getUser: jest.fn(), updateUser: jest.fn(), @@ -98,12 +103,12 @@ describe('getScheduledNotifs', () => { const isScheduling = false; const scheduledPromise = Promise.resolve(); // create the mock notifs from cordova plugin - const mockNotifs = [{ trigger: { at: DateTime.now().toMillis() } }]; + const mockNotifs = [{ trigger: { at: DateTime.now().toJSDate() } }]; // create the expected result const expectedResult = [ { - key: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[0].trigger.at).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[0].trigger.at).toFormat('t'), }, ]; @@ -122,12 +127,12 @@ describe('getScheduledNotifs', () => { const isScheduling = true; const scheduledPromise = Promise.resolve(); // create the mock notifs from cordova plugin - const mockNotifs = [{ trigger: { at: DateTime.now().toMillis() } }]; + const mockNotifs = [{ trigger: { at: DateTime.now().toJSDate() } }]; // create the expected result const expectedResult = [ { - key: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[0].trigger.at).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[0].trigger.at).toFormat('t'), }, ]; @@ -166,36 +171,36 @@ describe('getScheduledNotifs', () => { const scheduledPromise = Promise.resolve(); // create the mock notifs from cordova plugin (greater than 5 notifications) const mockNotifs = [ - { trigger: { at: DateTime.now().toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 1 }).toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 2 }).toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 3 }).toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 4 }).toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 5 }).toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 6 }).toMillis() } }, - { trigger: { at: DateTime.now().plus({ weeks: 7 }).toMillis() } }, + { trigger: { at: DateTime.now().toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 1 }).toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 2 }).toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 3 }).toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 4 }).toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 5 }).toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 6 }).toJSDate() } }, + { trigger: { at: DateTime.now().plus({ weeks: 7 }).toJSDate() } }, ]; // create the expected result (only the first 5 notifications) const expectedResult = [ { - key: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[0].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[0].trigger.at as Date).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[0].trigger.at as Date).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifs[1].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[1].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[1].trigger.at as Date).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[1].trigger.at as Date).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifs[2].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[2].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[2].trigger.at as Date).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[2].trigger.at as Date).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifs[3].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[3].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[3].trigger.at as Date).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[3].trigger.at as Date).toFormat('t'), }, { - key: DateTime.fromMillis(mockNotifs[4].trigger.at).toFormat('DDD'), - val: DateTime.fromMillis(mockNotifs[4].trigger.at).toFormat('t'), + key: DateTime.fromJSDate(mockNotifs[4].trigger.at as Date).toFormat('DDD'), + val: DateTime.fromJSDate(mockNotifs[4].trigger.at as Date).toFormat('t'), }, ]; @@ -237,7 +242,7 @@ describe('updateScheduledNotifs', () => { const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); const scheduledPromise: Promise = Promise.resolve(); // create an empty array of mock notifs from cordova plugin - const mockNotifs = []; + let mockNotifs = []; // mock the cordova plugin jest @@ -245,17 +250,30 @@ describe('updateScheduledNotifs', () => { .mockImplementation((callback) => callback(mockNotifs)); jest .spyOn(window['cordova'].plugins.notification.local, 'cancelAll') - .mockImplementation((callback) => callback()); + .mockImplementation((callback) => { + mockNotifs = []; + callback(); + }); jest .spyOn(window['cordova'].plugins.notification.local, 'schedule') - .mockImplementation((arg, callback) => callback(arg)); + .mockImplementation((arg, callback) => { + arg.forEach((notif) => { + mockNotifs.push(notif); + }); + console.log('called mockNotifs.concat(arg)', mockNotifs); + callback(arg); + }); // call the function await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); + const scheduledNotifs = await getScheduledNotifs(isScheduling, scheduledPromise); expect(setIsScheduling).toHaveBeenCalledWith(true); expect(logDebug).toHaveBeenCalledWith('After cancelling, there are no scheduled notifications'); - expect(logDebug).toHaveBeenCalledWith('After scheduling, there are no scheduled notifications'); + expect(logDebug).toHaveBeenCalledWith( + 'After scheduling, there are 4 scheduled notifications at 21:00 first is November 19, 2023 at 9:00 PM', + ); expect(setIsScheduling).toHaveBeenCalledWith(false); + expect(scheduledNotifs).toHaveLength(4); }); it('should resolve without scheduling if notifications are already scheduled', async () => { @@ -267,10 +285,10 @@ describe('updateScheduledNotifs', () => { // create the mock notifs from cordova plugin (must match the notifs that will generate from the reminder scheme above... // in this case: exampleReminderSchemes.weekly, because getUser is mocked to return reminder_assignment: 'weekly') const mockNotifs = [ - { trigger: { at: DateTime.fromFormat('2023-11-14 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, - { trigger: { at: DateTime.fromFormat('2023-11-15 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, - { trigger: { at: DateTime.fromFormat('2023-11-17 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, - { trigger: { at: DateTime.fromFormat('2023-11-19 21:00', 'yyyy-MM-dd HH:mm').toMillis() } }, + { trigger: { at: DateTime.fromFormat('2023-11-14 21:00', 'yyyy-MM-dd HH:mm').toJSDate() } }, + { trigger: { at: DateTime.fromFormat('2023-11-15 21:00', 'yyyy-MM-dd HH:mm').toJSDate() } }, + { trigger: { at: DateTime.fromFormat('2023-11-17 21:00', 'yyyy-MM-dd HH:mm').toJSDate() } }, + { trigger: { at: DateTime.fromFormat('2023-11-19 21:00', 'yyyy-MM-dd HH:mm').toJSDate() } }, ]; // mock the cordova plugin diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 0111dba26..15adb2521 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -33,7 +33,7 @@ const calcNotifTimes = (scheme, dayZeroDate, timeOfDay): DateTime[] => { // returns true if all expected times are already scheduled const areAlreadyScheduled = (notifs: any[], expectedTimes: DateTime[]) => { for (const t of expectedTimes) { - if (!notifs.some((n) => DateTime.fromMillis(n.trigger.at).equals(t))) { + if (!notifs.some((n) => DateTime.fromJSDate(n.trigger.at).equals(t))) { return false; } } @@ -54,12 +54,12 @@ const areAlreadyScheduled = (notifs: any[], expectedTimes: DateTime[]) => { function debugGetScheduled(prefix) { window['cordova'].plugins.notification.local.getScheduled((notifs) => { if (!notifs?.length) return logDebug(`${prefix}, there are no scheduled notifications`); - const time = DateTime.fromMillis(notifs?.[0].trigger.at).toFormat('HH:mm'); + const time = DateTime.fromJSDate(notifs[0].trigger.at).toFormat('HH:mm'); //was in plugin, changed to scheduler let scheduledNotifs = []; scheduledNotifs = notifs.map((n) => { - const time = DateTime.fromMillis(n.trigger.at).toFormat('t'); - const date = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + const date = DateTime.fromJSDate(n.trigger.at).toFormat('DDD'); + const time = DateTime.fromJSDate(n.trigger.at).toFormat('t'); return { key: date, val: time, @@ -109,8 +109,8 @@ const getNotifs = function () { const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing let scheduledNotifs = []; scheduledNotifs = notifSubset.map((n) => { - const time: string = DateTime.fromMillis(n.trigger.at).toFormat('t'); - const date: string = DateTime.fromMillis(n.trigger.at).toFormat('DDD'); + const time: string = DateTime.fromJSDate(n.trigger.at).toFormat('t'); + const date: string = DateTime.fromJSDate(n.trigger.at).toFormat('DDD'); return { key: date, val: time,