diff --git a/components/settings/customization/advance-notice-days-slider.js b/components/settings/customization/advance-notice-days-slider.js new file mode 100644 index 0000000..ece11f0 --- /dev/null +++ b/components/settings/customization/advance-notice-days-slider.js @@ -0,0 +1,56 @@ +import React from 'react' +import { View } from 'react-native' +import PropTypes from 'prop-types' +import Slider from '@ptomasroos/react-native-multi-slider' +import SliderLabel from './slider-label' + +import { styles } from './slider-styles' +import { + ADVANCE_PERIOD_NOTICE_DAYS_MIN, + ADVANCE_PERIOD_NOTICE_DAYS_MAX, +} from '../../../config' + +const AdvanceNoticeDaysSlider = ({ + disabled, + advanceNoticeDays, + onAdvanceNoticeDaysChange, +}) => { + const sliderAccentBackground = disabled + ? styles.disabledSliderAccentBackground + : styles.sliderAccentBackground + + const sliderBackground = disabled + ? styles.disabledSliderBackground + : styles.sliderBackground + + return ( + + + + ) +} + +export default AdvanceNoticeDaysSlider + +AdvanceNoticeDaysSlider.propTypes = { + disabled: PropTypes.bool, + advanceNoticeDays: PropTypes.number, + onAdvanceNoticeDaysChange: PropTypes.func, +} diff --git a/components/settings/customization/slider-styles.js b/components/settings/customization/slider-styles.js new file mode 100644 index 0000000..e28f31e --- /dev/null +++ b/components/settings/customization/slider-styles.js @@ -0,0 +1,35 @@ +import { StyleSheet } from 'react-native' +import { Colors, Sizes } from '../../../styles' + +export const styles = StyleSheet.create({ + container: { + alignItems: 'center', + paddingTop: Sizes.base, + }, + marker: { + backgroundColor: Colors.turquoiseDark, + + borderRadius: 50, + elevation: 4, + height: Sizes.subtitle, + width: Sizes.subtitle, + }, + slider: { + borderRadius: 25, + height: Sizes.small, + paddingTop: Sizes.base, + }, + sliderAccentBackground: { + backgroundColor: Colors.turquoiseDark, + }, + disabledSliderAccentBackground: { + backgroundColor: Colors.grey, + }, + sliderBackground: { + backgroundColor: Colors.turquoise, + }, + disabledSliderBackground: { + backgroundColor: Colors.greyLight, + }, + markerOffsetY: Sizes.tiny, +}) diff --git a/components/settings/customization/temperature-slider.js b/components/settings/customization/temperature-slider.js index 7d91f7a..463b001 100644 --- a/components/settings/customization/temperature-slider.js +++ b/components/settings/customization/temperature-slider.js @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { StyleSheet, View } from 'react-native' +import { View } from 'react-native' import PropTypes from 'prop-types' import Slider from '@ptomasroos/react-native-multi-slider' @@ -7,9 +7,9 @@ import alertError from '../common/alert-error' import SliderLabel from './slider-label' import { scaleObservable, saveTempScale } from '../../../local-storage' -import { Colors, Sizes } from '../../../styles' import labels from '../../../i18n/en/settings' import { TEMP_MIN, TEMP_MAX, TEMP_SLIDER_STEP } from '../../../config' +import { styles } from './slider-styles' const TemperatureSlider = ({ disabled }) => { const savedValue = scaleObservable.value @@ -40,7 +40,7 @@ const TemperatureSlider = ({ disabled }) => { customLabel={SliderLabel} enableLabel={true} markerStyle={styles.marker} - markerOffsetY={Sizes.tiny} + markerOffsetY={styles.markerOffsetY} max={TEMP_MAX} min={TEMP_MIN} onValuesChange={onTemperatureSliderChange} @@ -61,34 +61,3 @@ export default TemperatureSlider TemperatureSlider.propTypes = { disabled: PropTypes.bool, } - -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - paddingTop: Sizes.base, - }, - marker: { - backgroundColor: Colors.turquoiseDark, - - borderRadius: 50, - elevation: 4, - height: Sizes.subtitle, - width: Sizes.subtitle, - }, - slider: { - borderRadius: 25, - height: Sizes.small, - }, - sliderAccentBackground: { - backgroundColor: Colors.turquoiseDark, - }, - disabledSliderAccentBackground: { - backgroundColor: Colors.grey, - }, - sliderBackground: { - backgroundColor: Colors.turquoise, - }, - disabledSliderBackground: { - backgroundColor: Colors.greyLight, - }, -}) diff --git a/components/settings/reminders/period-reminder.js b/components/settings/reminders/period-reminder.js new file mode 100644 index 0000000..b0bf81f --- /dev/null +++ b/components/settings/reminders/period-reminder.js @@ -0,0 +1,54 @@ +import React, { useState } from 'react' +import AppSwitch from '../../common/app-switch' +import AdvanceNoticeDaysSlider from '../customization/advance-notice-days-slider' + +import { + periodReminderObservable, + savePeriodReminder, + periodPredictionObservable, + saveAdvanceNoticeDays, + advanceNoticeDaysObservable, +} from '../../../local-storage' +import labels from '../../../i18n/en/settings' + +const PeriodReminder = () => { + const isPeriodPredictionDisabled = !periodPredictionObservable.value + + const [isPeriodReminderEnabled, setIsPeriodReminderEnabled] = useState( + periodReminderObservable.value.enabled + ) + + const [advanceNoticeDays, setAdvanceNoticeDays] = useState( + advanceNoticeDaysObservable.value + ) + + const periodReminderToggle = (isEnabled) => { + setIsPeriodReminderEnabled(isEnabled) + savePeriodReminder({ enabled: isEnabled }) + } + + const handleAdvanceNoticeDaysChange = (days) => { + setAdvanceNoticeDays(days) + saveAdvanceNoticeDays(days) + } + + return ( + <> + + {isPeriodReminderEnabled && ( + + )} + + ) +} + +export default PeriodReminder diff --git a/components/settings/reminders/reminders.js b/components/settings/reminders/reminders.js index 1279d29..63f2cbb 100644 --- a/components/settings/reminders/reminders.js +++ b/components/settings/reminders/reminders.js @@ -1,13 +1,11 @@ -import React, { useState } from 'react' +import React from 'react' import AppPage from '../../common/app-page' -import AppSwitch from '../../common/app-switch' import Segment from '../../common/segment' import TemperatureReminder from './temperature-reminder' +import PeriodReminder from './period-reminder' import { - periodReminderObservable, - savePeriodReminder, periodPredictionObservable, temperatureTrackingCategoryObservable, } from '../../../local-storage' @@ -16,17 +14,7 @@ import labels from '../../../i18n/en/settings' import { Alert, Pressable } from 'react-native' const Reminders = () => { - const isPeriodPredictionDisabled = !periodPredictionObservable.value - - const [isPeriodReminderEnabled, setIsPeriodReminderEnabled] = useState( - periodReminderObservable.value.enabled - ) - const periodReminderToggle = (isEnabled) => { - setIsPeriodReminderEnabled(isEnabled) - savePeriodReminder({ enabled: isEnabled }) - } - - const reminderDisabledPrompt = () => { + const periodReminderDisabledPrompt = () => { if (!periodPredictionObservable.value) { Alert.alert( labels.periodReminder.alertNoPeriodReminder.title, @@ -43,16 +31,12 @@ const Reminders = () => { ) } } + return ( - + - + diff --git a/config.js b/config.js index 5d11e5d..fa854f6 100644 --- a/config.js +++ b/config.js @@ -33,6 +33,10 @@ export const TEMP_MAX = 39 export const TEMP_MIN = 35 export const TEMP_SLIDER_STEP = 0.5 +export const ADVANCE_PERIOD_NOTICE_DAYS_MIN = 1 +export const ADVANCE_PERIOD_NOTICE_DAYS_MAX = 7 +export const ADVANCE_PERIOD_NOTICE_DAYS_INIT_VALUE = 3 + export const HIT_SLOP = { top: verticalScale(20), bottom: verticalScale(20), diff --git a/i18n/en/settings.js b/i18n/en/settings.js index 7f00368..7c09c14 100644 --- a/i18n/en/settings.js +++ b/i18n/en/settings.js @@ -60,10 +60,13 @@ export default { }, periodReminder: { title: 'Next period reminder', - reminderText: - 'Get a notification 3 days before your next period is likely to start.', - notification: (daysToEndOfPrediction) => - `Your next period is likely to start in 3 to ${daysToEndOfPrediction} days.`, + reminderText: (days) => { + const dayCount = parseInt(days, 10) + const dayText = dayCount === 1 ? '1 day' : `${dayCount} days` + return `Get a notification ${dayText} before your next period is likely to start.` + }, + notification: (advanceNoticeDays, daysToEndOfPrediction) => + `Your next period is likely to start in ${advanceNoticeDays} to ${daysToEndOfPrediction} days.`, alertNoPeriodReminder: { title: 'Period predictions turned off', message: diff --git a/lib/notifications.js b/lib/notifications.js index d34d752..566ac0d 100644 --- a/lib/notifications.js +++ b/lib/notifications.js @@ -2,9 +2,9 @@ import { Platform } from 'react-native' import { tempReminderObservable, periodReminderObservable, + advanceNoticeDaysObservable, } from '../local-storage' import * as PN from 'react-native-push-notification' -import { requestNotifications } from 'react-native-permissions' import Moment from 'moment' import { LocalDate } from '@js-joda/core' @@ -13,12 +13,14 @@ import { getBleedingDaysSortedByDate } from '../db' import cycleModule from './cycle' import nothingChanged from '../db/db-unchanged' -export default function setupNotifications(navigate, setDate) { - Platform.OS === 'android' ? requestNotifications() : null - const PushNotification = Platform.OS === 'ios' ? PN : PN.default +const DRIP_CHANNEL_ID = 'drip-channel-id' +const TEMPERATURE_REMINDER_ID = '1' +const PERIOD_REMINDER_ID = '2' +const PushNotification = Platform.OS === 'ios' ? PN : PN.default +export default function setupNotifications(navigate, setDate) { PushNotification.createChannel({ - channelId: 'drip-channel-id', // (required) + channelId: DRIP_CHANNEL_ID, // (required) channelName: 'drip reminder', // (required) playSound: false, // (optional) default: true }) @@ -26,7 +28,10 @@ export default function setupNotifications(navigate, setDate) { PushNotification.configure({ onNotification: (notification) => { // https://github.com/zo0r/react-native-push-notification/issues/966#issuecomment-479069106 - if (notification.data?.id === '1' || notification.id === '1') { + if ( + notification.data?.id === TEMPERATURE_REMINDER_ID || + notification.id === TEMPERATURE_REMINDER_ID + ) { const todayDate = LocalDate.now().toString() setDate(todayDate) navigate('TemperatureEditView') @@ -37,7 +42,7 @@ export default function setupNotifications(navigate, setDate) { }) tempReminderObservable((reminder) => { - PushNotification.cancelLocalNotification({ id: '1' }) + PushNotification.cancelLocalNotification({ id: TEMPERATURE_REMINDER_ID }) if (reminder.enabled) { const [hours, minutes] = reminder.time.split(':') let target = new Moment() @@ -50,57 +55,74 @@ export default function setupNotifications(navigate, setDate) { } PushNotification.localNotificationSchedule({ - id: '1', - userInfo: { id: '1' }, + id: TEMPERATURE_REMINDER_ID, + userInfo: { id: TEMPERATURE_REMINDER_ID }, message: labels.tempReminder.notification, date: target.toDate(), vibrate: false, repeatType: 'day', - channelId: 'drip-channel-id', + channelId: DRIP_CHANNEL_ID, allowWhileIdle: true, }) } }, false) - periodReminderObservable((reminder) => { - PushNotification.cancelLocalNotification({ id: '2' }) - if (reminder.enabled) setupPeriodReminder() - }, false) + periodReminderObservable(() => updatePeriodNotification(), false) + advanceNoticeDaysObservable(() => updatePeriodNotification(), false) getBleedingDaysSortedByDate().addListener((_, changes) => { // the listener fires on setup, so we check if there were actually any changes - if (nothingChanged(changes)) return - PushNotification.cancelLocalNotification({ id: '2' }) - if (periodReminderObservable.value.enabled) setupPeriodReminder() + if (nothingChanged(changes)) { + return + } + + updatePeriodNotification() }) } -function setupPeriodReminder() { - const PushNotification = Platform.OS === 'ios' ? PN : PN.default +const updatePeriodNotification = () => { + // Cancel any existing period reminder + PushNotification.cancelLocalNotification({ id: PERIOD_REMINDER_ID }) + + // Set up a new period reminder if enabled + if (periodReminderObservable.value.enabled) { + schedulePeriodNotification() + } +} + +function schedulePeriodNotification() { const bleedingPrediction = cycleModule().getPredictedMenses() + if (bleedingPrediction.length > 0) { const predictedBleedingStart = Moment( bleedingPrediction[0][0], 'YYYY-MM-DD' ) - // 3 days before and at 6 am + + const advanceNoticeDays = parseInt(advanceNoticeDaysObservable.value, 10) + + // ${advanceNoticeDays} days before and at 6 am const reminderDate = predictedBleedingStart - .subtract(3, 'days') + .subtract(advanceNoticeDays, 'days') .hours(6) .minutes(0) .seconds(0) if (reminderDate.isAfter()) { - // period is likely to start in 3 to 3 + (length of prediction - 1) days - const daysToEndOfPrediction = bleedingPrediction[0].length + 2 + // period is likely to start in advanceNoticeDays to advanceNoticeDays + (length of prediction - 1) days + const daysToEndOfPrediction = + advanceNoticeDays + bleedingPrediction[0].length - 1 PushNotification.localNotificationSchedule({ - id: '2', - userInfo: { id: '2' }, - message: labels.periodReminder.notification(daysToEndOfPrediction), + id: PERIOD_REMINDER_ID, + userInfo: { id: PERIOD_REMINDER_ID }, + message: labels.periodReminder.notification( + advanceNoticeDays, + daysToEndOfPrediction + ), date: reminderDate.toDate(), vibrate: false, - channelId: 'drip-channel-id', + channelId: DRIP_CHANNEL_ID, allowWhileIdle: true, }) } diff --git a/local-storage.js b/local-storage.js index 66ce305..63bd55a 100644 --- a/local-storage.js +++ b/local-storage.js @@ -2,6 +2,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import Observable from 'obv' import { TEMP_SCALE_MIN, TEMP_SCALE_MAX, TEMP_SCALE_UNITS } from './config' +import { ADVANCE_PERIOD_NOTICE_DAYS_INIT_VALUE } from './config' + export const scaleObservable = Observable() setObvWithInitValue('tempScale', scaleObservable, { min: TEMP_SCALE_MIN, @@ -59,6 +61,18 @@ export async function savePeriodPrediction(bool) { } } +export const advanceNoticeDaysObservable = Observable() +setObvWithInitValue( + 'advanceNoticeDays', + advanceNoticeDaysObservable, + parseInt(ADVANCE_PERIOD_NOTICE_DAYS_INIT_VALUE, 10) +) + +export async function saveAdvanceNoticeDays(days) { + await AsyncStorage.setItem('advanceNoticeDays', JSON.stringify(days)) + advanceNoticeDaysObservable.set(days) +} + export const useCervixAsSecondarySymptomObservable = Observable() setObvWithInitValue( 'useCervixAsSecondarySymptom', @@ -109,7 +123,7 @@ export async function saveTemperatureTrackingCategory(bool) { if (!temperatureTrackingCategoryObservable.value) { // if temperature tracking is turned off, the temperature reminder gets disabled const tempReminderResult = await AsyncStorage.getItem('tempReminder') - if (tempReminderResult && JSON.parse(tempReminderResult).enabled) { + if (tempReminderResult && JSON.parse(tempReminderResult).enabled) { tempReminderObservable.set(false) } }