diff --git a/android/app/build.gradle b/android/app/build.gradle index fa56b47..6348b76 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -138,6 +138,7 @@ android { } dependencies { + compile project(':react-native-push-notification') compile project(':react-native-vector-icons') compile project(':react-native-fs') compile project(':react-native-document-picker') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4654ff4..207d90a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,15 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/drip/MainApplication.java b/android/app/src/main/java/com/drip/MainApplication.java index 4dc63a4..da1f79f 100644 --- a/android/app/src/main/java/com/drip/MainApplication.java +++ b/android/app/src/main/java/com/drip/MainApplication.java @@ -3,6 +3,7 @@ package com.drip; import android.app.Application; import com.facebook.react.ReactApplication; +import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage; import com.oblador.vectoricons.VectorIconsPackage; import com.rnfs.RNFSPackage; import com.reactnativedocumentpicker.ReactNativeDocumentPicker; @@ -29,6 +30,7 @@ public class MainApplication extends Application implements ReactApplication, Sh protected List getPackages() { return Arrays.asList( new MainReactPackage(), + new ReactNativePushNotificationPackage(), new VectorIconsPackage(), new RNFSPackage(), new ReactNativeDocumentPicker(), diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..5840582 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..003156a Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..1bdb4bd Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..9af70b0 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..45e6624 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index a2f5908..bb93aa9 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 1b52399..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_notification.png b/android/app/src/main/res/mipmap-hdpi/ic_notification.png new file mode 100644 index 0000000..5840582 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index ff10afd..d221c45 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 115a4c7..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_notification.png b/android/app/src/main/res/mipmap-mdpi/ic_notification.png new file mode 100644 index 0000000..003156a Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index dcd3cd8..3be181b 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 459ca60..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xhdpi/ic_notification.png new file mode 100644 index 0000000..1bdb4bd Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8ca12fe..05f21c4 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 8e19b41..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png new file mode 100644 index 0000000..9af70b0 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index b824ebd..3a12ebe 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 4c19a13..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..45e6624 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png differ diff --git a/android/settings.gradle b/android/settings.gradle index 6a9c8c8..fe009fa 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'drip' +include ':react-native-push-notification' +project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android') include ':react-native-vector-icons' project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') include ':react-native-fs' diff --git a/components/app.js b/components/app.js index b0924aa..827b11e 100644 --- a/components/app.js +++ b/components/app.js @@ -10,6 +10,7 @@ import Chart from './chart/chart' import Settings from './settings' import Stats from './stats' import {headerTitles as titles} from './labels' +import setupNotifications from '../lib/notifications' const isSymptomView = name => Object.keys(symptomViews).indexOf(name) > -1 @@ -19,28 +20,28 @@ export default class App extends Component { this.state = { currentPage: 'Home' } - - const handleBackButtonPress = function() { - if (this.state.currentPage === 'Home') return false - if (isSymptomView(this.state.currentPage)) { - this.navigate('CycleDay', {cycleDay: this.state.currentProps.cycleDay}) - } else { - this.navigate('Home') - } - return true - }.bind(this) - - this.backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackButtonPress) + this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackButtonPress) + setupNotifications(this.navigate) } componentWillUnmount() { this.backHandler.remove() } - navigate(pageName, props) { + navigate = (pageName, props) => { this.setState({currentPage: pageName, currentProps: props}) } + handleBackButtonPress = () => { + if (this.state.currentPage === 'Home') return false + if (isSymptomView(this.state.currentPage)) { + this.navigate('CycleDay', { cycleDay: this.state.currentProps.cycleDay }) + } else { + this.navigate('Home') + } + return true + } + render() { const page = { Home, Calendar, CycleDay, Chart, Settings, Stats, ...symptomViews @@ -51,14 +52,14 @@ export default class App extends Component { {this.state.currentPage != 'CycleDay' &&
} {React.createElement(page, { - navigate: this.navigate.bind(this), + navigate: this.navigate, ...this.state.currentProps })} {!isSymptomView(this.state.currentPage) && - + } ) } -} \ No newline at end of file +} diff --git a/components/calendar.js b/components/calendar.js index 72fb1ba..fd85d3a 100644 --- a/components/calendar.js +++ b/components/calendar.js @@ -25,7 +25,7 @@ export default class CalendarView extends Component { bleedingDaysSortedByDate.removeListener(this.setStateWithCalFormattedDays) } - passDateToDayView(result) { + passDateToDayView = (result) => { const cycleDay = getOrCreateCycleDay(result.dateString) const navigate = this.props.navigate navigate('CycleDay', { cycleDay }) @@ -34,7 +34,7 @@ export default class CalendarView extends Component { render() { return ( diff --git a/components/cycle-day/cycle-day-overview.js b/components/cycle-day/cycle-day-overview.js index d94453e..a75dda3 100644 --- a/components/cycle-day/cycle-day-overview.js +++ b/components/cycle-day/cycle-day-overview.js @@ -33,7 +33,7 @@ export default class CycleDayOverView extends Component { } } - goToCycleDay(target) { + goToCycleDay = (target) => { const localDate = LocalDate.parse(this.state.cycleDay.date) const targetDate = target === 'before' ? localDate.minusDays(1).toString() : @@ -57,7 +57,7 @@ export default class CycleDayOverView extends Component { isCycleDayOverView={true} cycleDayNumber={cycleDayNumber} date={cycleDay.date} - goToCycleDay={this.goToCycleDay.bind(this)} + goToCycleDay={this.goToCycleDay} /> diff --git a/components/labels.js b/components/labels.js index 7608b62..b398a4f 100644 --- a/components/labels.js +++ b/components/labels.js @@ -42,6 +42,12 @@ export const settings = { max: 'Max', loadError: 'Could not load saved temperature scale settings', saveError: 'Could not save temperature scale settings' + }, + tempReminder: { + title: 'Temperature reminder', + noTimeSet: 'Set a time for a daily reminder to take your temperature', + timeSet: time => `Daily reminder set for ${time}`, + notification: 'Record your morning temperature' } } diff --git a/components/menu.js b/components/menu.js index 8a55187..737203b 100644 --- a/components/menu.js +++ b/components/menu.js @@ -8,7 +8,7 @@ import styles, { iconStyles } from '../styles' import Icon from 'react-native-vector-icons/MaterialCommunityIcons' export default class Menu extends Component { - makeMenuItem({ title, icon, onPress}, i) { + makeMenuItem = ({ title, icon, onPress}, i) => { return ( this.goTo('Chart') }, { title: 'Stats', icon: 'chart-pie', onPress: () => this.goTo('Stats') }, { title: 'Settings', icon: 'settings', onPress: () => this.goTo('Settings') }, - ].map(this.makeMenuItem.bind(this))} + ].map(this.makeMenuItem)} ) } diff --git a/components/settings.js b/components/settings.js index f03aec9..aac1a65 100644 --- a/components/settings.js +++ b/components/settings.js @@ -4,53 +4,66 @@ import { TouchableOpacity, ScrollView, Alert, - Text + Text, + Switch } from 'react-native' +import DateTimePicker from 'react-native-modal-datetime-picker-nevo' import Slider from '@ptomasroos/react-native-multi-slider' import Share from 'react-native-share' import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' import rnfs from 'react-native-fs' import styles, { secondaryColor } from '../styles/index' import config from '../config' -import { settings as settingsLabels, shared as sharedLabels } from './labels' +import { settings as labels, shared as sharedLabels } from './labels' import getDataAsCsvDataUri from '../lib/import-export/export-to-csv' import importCsv from '../lib/import-export/import-from-csv' -import { scaleObservable, saveTempScale } from '../local-storage' +import { + scaleObservable, + saveTempScale, + tempReminderObservable, + saveTempReminder +} from '../local-storage' export default class Settings extends Component { + constructor(props) { + super(props) + this.state = {} + } + render() { return ( + - {settingsLabels.tempScale.segmentTitle} + {labels.tempScale.segmentTitle} - {settingsLabels.tempScale.segmentExplainer} + {labels.tempScale.segmentExplainer} - {settingsLabels.export.button} + {labels.export.button} - {settingsLabels.export.segmentExplainer} + {labels.export.segmentExplainer} - {settingsLabels.export.button} + {labels.export.button} - {settingsLabels.import.button} + {labels.import.button} - {settingsLabels.import.segmentExplainer} + {labels.import.segmentExplainer} - {settingsLabels.import.button} + {labels.import.button} @@ -59,6 +72,66 @@ export default class Settings extends Component { } } +class TempReminderPicker extends Component { + constructor(props) { + super(props) + this.state = Object.assign({}, tempReminderObservable.value) + } + + render() { + return ( + this.setState({ isTimePickerVisible: true })} + > + + {labels.tempReminder.title} + + + + {this.state.time && this.state.enabled ? + {labels.tempReminder.timeSet(this.state.time)} + : + {labels.tempReminder.noTimeSet} + } + + { + this.setState({ enabled: switchOn }) + if (switchOn && !this.state.time) { + this.setState({ isTimePickerVisible: true }) + } + if (!switchOn) saveTempReminder({ enabled: false }) + }} + onTintColor={secondaryColor} + /> + { + const time = padWithZeros(`${jsDate.getHours()}:${jsDate.getMinutes()}`) + this.setState({ + time, + isTimePickerVisible: false, + enabled: true + }) + saveTempReminder({ + time, + enabled: true + }) + }} + onCancel={() => { + this.setState({ isTimePickerVisible: false }) + if (!this.state.time) this.setState({enabled: false}) + }} + /> + + + ) + } +} + class TempSlider extends Component { constructor(props) { super(props) @@ -80,15 +153,15 @@ class TempSlider extends Component { try { saveTempScale(this.state) } catch(err) { - alertError(settingsLabels.tempScale.saveError) + alertError(labels.tempScale.saveError) } } render() { return ( - - {`${settingsLabels.tempScale.min} ${this.state.min}`} - {`${settingsLabels.tempScale.max} ${this.state.max}`} + + {`${labels.tempScale.min} ${this.state.min}`} + {`${labels.tempScale.max} ${this.state.max}`} getFileContentAndImport({ deleteExisting: false }) }, { - text: settingsLabels.import.deleteOption, + text: labels.import.deleteOption, onPress: () => getFileContentAndImport({ deleteExisting: true }) }, { text: sharedLabels.cancel, style: 'cancel', onPress: () => { } @@ -180,12 +253,12 @@ async function getFileContentAndImport({ deleteExisting }) { try { fileContent = await rnfs.readFile(fileInfo.uri, 'utf8') } catch (err) { - return importError(settingsLabels.import.errors.couldNotOpenFile) + return importError(labels.import.errors.couldNotOpenFile) } try { await importCsv(fileContent, deleteExisting) - Alert.alert(sharedLabels.successTitle, settingsLabels.import.success.message) + Alert.alert(sharedLabels.successTitle, labels.import.success.message) } catch(err) { importError(err.message) } @@ -196,6 +269,16 @@ function alertError(msg) { } function importError(msg) { - const postFixed = `${msg}\n\n${settingsLabels.import.errors.postFix}` + const postFixed = `${msg}\n\n${labels.import.errors.postFix}` alertError(postFixed) } + +function padWithZeros(time) { + const vals = time.split(':') + return vals.map(val => { + if (parseInt(val) < 10) { + val = `0${val}` + } + return val + }).join(':') +} \ No newline at end of file diff --git a/lib/notifications.js b/lib/notifications.js new file mode 100644 index 0000000..dc1a7a5 --- /dev/null +++ b/lib/notifications.js @@ -0,0 +1,38 @@ +import {tempReminderObservable} from '../local-storage' +import Notification from 'react-native-push-notification' +import { LocalDate } from 'js-joda' +import Moment from 'moment' +import { settings as labels } from '../components/labels' +import { getOrCreateCycleDay } from '../db' + +export default function setupNotifications(navigate) { + Notification.configure({ + onNotification: () => { + const todayDateString = LocalDate.now().toString() + const cycleDay = getOrCreateCycleDay(todayDateString) + navigate('TemperatureEditView', { cycleDay }) + } + }) + + tempReminderObservable(reminder => { + Notification.cancelAllLocalNotifications() + if (reminder.enabled) { + const [hours, minutes] = reminder.time.split(':') + let target = new Moment() + .hours(parseInt(hours)) + .minutes(parseInt(minutes)) + .seconds(0) + + if(target.isBefore(new Moment())) { + target = target.add(1, 'd') + } + + Notification.localNotificationSchedule({ + message: labels.tempReminder.notification, + date: target.toDate(), + vibrate: false, + repeatType: 'day' + }) + } + }) +} \ No newline at end of file diff --git a/local-storage/index.js b/local-storage/index.js index 50d886f..6296271 100644 --- a/local-storage/index.js +++ b/local-storage/index.js @@ -3,20 +3,25 @@ import Observable from 'obv' import config from '../config' export const scaleObservable = Observable() -setTempScaleInitially() +setObvWithInitValue('tempScale', scaleObservable, { + min: config.temperatureScale.defaultLow, + max: config.temperatureScale.defaultHigh +}) -async function setTempScaleInitially() { - const result = await AsyncStorage.getItem('tempScale') +export const tempReminderObservable = Observable() +setObvWithInitValue('tempReminder', tempReminderObservable, { + enabled: false +}) + +async function setObvWithInitValue(key, obv, defaultValue) { + const result = await AsyncStorage.getItem(key) let value if (result) { value = JSON.parse(result) } else { - value = { - min: config.temperatureScale.defaultLow, - max: config.temperatureScale.defaultHigh - } + value = defaultValue } - scaleObservable.set(value) + obv.set(value) } export async function saveTempScale(scale) { @@ -24,3 +29,7 @@ export async function saveTempScale(scale) { scaleObservable.set(scale) } +export async function saveTempReminder(reminder) { + await AsyncStorage.setItem('tempReminder', JSON.stringify(reminder)) + tempReminderObservable.set(reminder) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4186e9c..036a5b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3768,8 +3768,7 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", @@ -3777,8 +3776,7 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -3881,8 +3879,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -3892,7 +3889,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4004,8 +4000,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -4120,7 +4115,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6679,6 +6673,11 @@ } } }, + "react-native-push-notification": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.1.1.tgz", + "integrity": "sha512-4+4yQXNPqh5IVvpSBmR4Cy/UeMjTcfE8KIJgEuT7pME97WK+aGPn6W3ybhOoXC1n+ZWKfrAlsHydLE4xfBZDJg==" + }, "react-native-safe-area-view": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.8.0.tgz", diff --git a/package.json b/package.json index 2c492c5..11c62a8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "isobject": "^3.0.1", "js-base64": "^2.4.8", "js-joda": "^1.8.2", - "moment": "^2.22.1", + "moment": "^2.22.2", "object-path": "^0.11.4", "obv": "0.0.1", "react": "16.4.1", @@ -31,6 +31,7 @@ "react-native-document-picker": "^2.1.0", "react-native-fs": "^2.10.14", "react-native-modal-datetime-picker-nevo": "^4.11.0", + "react-native-push-notification": "^3.1.1", "react-native-share": "^1.1.0", "react-native-simple-radio-button": "^2.7.1", "react-native-vector-icons": "^5.0.0",