From 70f44df31df66bf4a3717e407b261b7ec5a3378e Mon Sep 17 00:00:00 2001 From: Adam Odziemkowski Date: Fri, 15 Nov 2019 17:14:38 -0500 Subject: [PATCH 1/3] Add schedule to redux --- app/state/responses/responses.actions.js | 5 +++++ app/state/responses/responses.constants.js | 1 + app/state/responses/responses.reducer.js | 6 ++++++ app/state/responses/responses.reducers.test.js | 8 ++++++++ app/state/responses/responses.selectors.js | 11 ++--------- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/state/responses/responses.actions.js b/app/state/responses/responses.actions.js index cb543b473..e2ece29bd 100644 --- a/app/state/responses/responses.actions.js +++ b/app/state/responses/responses.actions.js @@ -69,3 +69,8 @@ export const setCurrentScreen = (activityId, screenIndex) => ({ screenIndex, }, }); + +export const setSchedule = schedule => ({ + type: RESPONSES_CONSTANTS.SET_SCHEDULE, + payload: schedule, +}); diff --git a/app/state/responses/responses.constants.js b/app/state/responses/responses.constants.js index 1281628b6..7c8f3b4b5 100644 --- a/app/state/responses/responses.constants.js +++ b/app/state/responses/responses.constants.js @@ -10,4 +10,5 @@ export default { ADD_TO_UPLOAD_QUEUE: 'ADD_TO_UPLOAD_QUEUE', SHIFT_UPLOAD_QUEUE: 'SHIFT_UPLOAD_QUEUE', SET_CURRENT_SCREEN: 'SET_CURRENT_SCREEN', + SET_SCHEDULE: 'SET_SCHEDULE', }; diff --git a/app/state/responses/responses.reducer.js b/app/state/responses/responses.reducer.js index d0e597e18..f23904112 100644 --- a/app/state/responses/responses.reducer.js +++ b/app/state/responses/responses.reducer.js @@ -10,6 +10,7 @@ export const initialState = { downloaded: 0, }, uploadQueue: [], + schedule: {}, }; export default (state = initialState, action = {}) => { @@ -105,6 +106,11 @@ export default (state = initialState, action = {}) => { ...state, uploadQueue: R.remove(0, 1, state.uploadQueue), }; + case RESPONSES_CONSTANTS.SET_SCHEDULE: + return { + ...state, + schedule: action.payload, + }; default: return state; } diff --git a/app/state/responses/responses.reducers.test.js b/app/state/responses/responses.reducers.test.js index ee7d48e98..573f47cd3 100644 --- a/app/state/responses/responses.reducers.test.js +++ b/app/state/responses/responses.reducers.test.js @@ -9,6 +9,7 @@ import { setAnswers, addToUploadQueue, shiftUploadQueue, + setSchedule, } from './responses.actions'; test('it has an initial state', () => { @@ -113,3 +114,10 @@ test('it adds to the upload queue', () => { uploadQueue: ['itemB'], }); }); + +test('it sets the schedule', () => { + expect(responsesReducer(initialState, setSchedule('schedule'))).toEqual({ + ...initialState, + schedule: 'schedule', + }); +}); diff --git a/app/state/responses/responses.selectors.js b/app/state/responses/responses.selectors.js index 346c01257..fe46312d3 100644 --- a/app/state/responses/responses.selectors.js +++ b/app/state/responses/responses.selectors.js @@ -12,6 +12,8 @@ export const downloadProgressSelector = R.path(['responses', 'downloadProgress'] export const inProgressSelector = R.path(['responses', 'inProgress']); +export const responseScheduleSelector = R.path(['responses', 'schedule']); + export const currentAppletResponsesSelector = createSelector( responsesSelector, R.path(['app', 'currentApplet']), @@ -27,15 +29,6 @@ export const currentAppletResponsesSelector = createSelector( }, ); -// Flatten the response history so that keys are activity ids -export const responsesGroupedByActivitySelector = createSelector( - responsesSelector, - responses => R.groupBy( - response => (response.meta ? `activity/${response.meta.activity['@id']}` : null), - responses, - ), -); - export const currentResponsesSelector = createSelector( R.path(['app', 'currentActivity']), inProgressSelector, From 0c1c02412b168d5c61fe72983c1594549cd5c630 Mon Sep 17 00:00:00 2001 From: Adam Odziemkowski Date: Wed, 20 Nov 2019 14:26:05 -0500 Subject: [PATCH 2/3] Schedule activities and notifications --- android/app/build.gradle | 1 + .../data/MainApplication.java | 2 + android/settings.gradle | 2 + .../ActivityList/ActivityDueDate.js | 16 +- .../ActivityList/ActivityListItem.js | 9 +- app/components/ActivityList/index.js | 3 - app/components/ActivityList/sortActivities.js | 28 +- app/components/ActivitySummary.js | 12 +- app/components/core/NotificationDot.js | 6 +- .../ActivityDetailsComponent.js | 1 + app/services/network.js | 6 + app/services/time.js | 162 ++++------- app/services/time.test.js | 259 ++++++++++-------- app/state/applets/applets.selectors.js | 86 +++--- app/state/applets/applets.thunks.js | 2 - app/state/responses/responses.thunks.js | 15 +- ios/MDCApp.xcodeproj/project.pbxproj | 26 ++ package.json | 1 + yarn.lock | 5 + 19 files changed, 320 insertions(+), 322 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ce1f74e7..72ffe7039 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -138,6 +138,7 @@ android { } dependencies { + implementation project(':react-native-localize') configurations.all { resolutionStrategy { dependencySubstitution { diff --git a/android/app/src/main/java/lab/childmindinstitute/data/MainApplication.java b/android/app/src/main/java/lab/childmindinstitute/data/MainApplication.java index 70a1b5bc3..cffa6dc04 100644 --- a/android/app/src/main/java/lab/childmindinstitute/data/MainApplication.java +++ b/android/app/src/main/java/lab/childmindinstitute/data/MainApplication.java @@ -3,6 +3,7 @@ import android.app.Application; import com.facebook.react.ReactApplication; +import com.reactcommunity.rnlocalize.RNLocalizePackage; import com.oblador.vectoricons.VectorIconsPackage; import com.reactnativecommunity.slider.ReactSliderPackage; import com.reactnativecommunity.netinfo.NetInfoPackage; @@ -36,6 +37,7 @@ public boolean getUseDeveloperSupport() { protected List getPackages() { return Arrays.asList( new MainReactPackage(), + new RNLocalizePackage(), new VectorIconsPackage(), new ReactSliderPackage(), new NetInfoPackage(), diff --git a/android/settings.gradle b/android/settings.gradle index 70e25c886..d57c79d65 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'MDCApp' +include ':react-native-localize' +project(':react-native-localize').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-localize/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-community_slider' diff --git a/app/components/ActivityList/ActivityDueDate.js b/app/components/ActivityList/ActivityDueDate.js index 18304513f..e06ba078e 100644 --- a/app/components/ActivityList/ActivityDueDate.js +++ b/app/components/ActivityList/ActivityDueDate.js @@ -1,17 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { StyleSheet } from 'react-native'; -import moment from 'moment'; import { colors } from '../../theme'; import { LittleText } from '../core'; - -const formatTime = (timestamp) => { - const time = moment(timestamp); - if (moment().isSame(time, 'day')) { - return moment(timestamp).format('[Today at] h:mm A'); - } - return moment(timestamp).format('MMMM D'); -}; +import { formatTime } from '../../services/time'; const styles = StyleSheet.create({ textStyles: { @@ -20,17 +12,17 @@ const styles = StyleSheet.create({ }); const ActivityDueDate = ({ activity }) => { - if (activity.lastResponseTimestamp < activity.lastScheduledTimestamp) { + if (activity.status === 'overdue') { return ( - {formatTime(activity.lastScheduledTimestamp)} + Due on: {formatTime(activity.lastScheduledTimestamp)} ); } if (activity.status === 'scheduled') { return ( - {formatTime(activity.nextScheduledTimestamp)} + Scheduled for: {formatTime(activity.nextScheduledTimestamp)} ); } diff --git a/app/components/ActivityList/ActivityListItem.js b/app/components/ActivityList/ActivityListItem.js index 61b782e8d..133b4d3e1 100644 --- a/app/components/ActivityList/ActivityListItem.js +++ b/app/components/ActivityList/ActivityListItem.js @@ -23,11 +23,6 @@ const styles = StyleSheet.create({ marginLeft: 16, fontFamily: theme.fontFamily, }, - notification: { - position: 'absolute', - top: 4, - right: 12, - }, sectionHeading: { marginTop: 20, marginBottom: 0, @@ -93,9 +88,7 @@ const ActivityRow = ({ activity, onPress }) => { {activity.isOverdue && ( - - - + )} ); diff --git a/app/components/ActivityList/index.js b/app/components/ActivityList/index.js index a3909a258..62597574c 100644 --- a/app/components/ActivityList/index.js +++ b/app/components/ActivityList/index.js @@ -3,14 +3,11 @@ import PropTypes from 'prop-types'; import { View } from 'react-native'; import sortActivities from './sortActivities'; import ActivityListItem from './ActivityListItem'; -import { Heading } from '../core'; const ActivityList = ({ applet, inProgress, onPressActivity }) => { const activities = sortActivities(applet.activities, inProgress); return ( - {/* Activities */} - {activities.map(activity => ( onPressActivity(activity)} diff --git a/app/components/ActivityList/sortActivities.js b/app/components/ActivityList/sortActivities.js index e1441b453..a46c5a369 100644 --- a/app/components/ActivityList/sortActivities.js +++ b/app/components/ActivityList/sortActivities.js @@ -1,6 +1,7 @@ import * as R from 'ramda'; +import moment from 'moment'; -const sortActivitiesAlpha = (a, b) => { +const compareByNameAlpha = (a, b) => { const nameA = a.name.en.toUpperCase(); // ignore upper and lowercase const nameB = b.name.en.toUpperCase(); // ignore upper and lowercase if (nameA < nameB) { @@ -12,7 +13,7 @@ const sortActivitiesAlpha = (a, b) => { return 0; }; -const sortBy = propName => (a, b) => a[propName] - b[propName]; +const compareByTimestamp = propName => (a, b) => moment(a[propName]) - moment(b[propName]); export const getUnscheduled = activityList => activityList.filter( activity => activity.nextScheduledTimestamp === null @@ -28,11 +29,15 @@ export const getCompleted = activityList => activityList.filter( export const getScheduled = activityList => activityList.filter( activity => activity.nextScheduledTimestamp !== null - && activity.lastResponseTimestamp >= activity.lastScheduledTimestamp, + && ( + moment(activity.lastResponseTimestamp) >= moment(activity.lastScheduledTimestamp) + || activity.lastScheduledTimestamp === null + || activity.lastResponseTimestamp === null + ), ); export const getOverdue = activityList => activityList.filter( - activity => activity.lastResponseTimestamp < activity.lastScheduledTimestamp, + activity => moment(activity.lastResponseTimestamp) < moment(activity.lastScheduledTimestamp), ); const addSectionHeader = (array, headerText) => (array.length > 0 @@ -41,20 +46,27 @@ const addSectionHeader = (array, headerText) => (array.length > 0 const addProp = (key, val, arr) => arr.map(obj => R.assoc(key, val, obj)); +// Sort the activities into buckets of "in-progress", "overdue", "scheduled", "unscheduled", +// and "completed". Inject header labels, e.g. "In Progress", before the activities that fit +// into that bucket. export default (activityList, inProgress) => { const inProgressKeys = Object.keys(inProgress); const inProgressActivities = activityList.filter( activity => inProgressKeys.includes(activity.id), ); + const notInProgress = activityList.filter(activity => !inProgressKeys.includes(activity.id)); - const overdue = getOverdue(notInProgress).sort(sortBy('lastScheduledTimestamp')).reverse(); - const scheduled = getScheduled(notInProgress).sort(sortBy('nextScheduledTimestamp')); - const unscheduled = getUnscheduled(notInProgress).sort(sortActivitiesAlpha); + + // Activities that are scheduled for that time. + const overdue = getOverdue(notInProgress).sort(compareByTimestamp('lastScheduledTimestamp')).reverse(); + // Should tell the user when it will be activated. + const scheduled = getScheduled(notInProgress).sort(compareByTimestamp('nextScheduledTimestamp')); + const unscheduled = getUnscheduled(notInProgress).sort(compareByNameAlpha); const completed = getCompleted(notInProgress).reverse(); return [ ...addSectionHeader(addProp('status', 'in-progress', inProgressActivities), 'In Progress'), - ...addSectionHeader(addProp('status', 'overdue', overdue), 'Overdue'), + ...addSectionHeader(addProp('status', 'overdue', overdue), 'Due'), ...addSectionHeader(addProp('status', 'scheduled', scheduled), 'Scheduled'), ...addSectionHeader(addProp('status', 'unscheduled', unscheduled), 'Unscheduled'), ...addSectionHeader(addProp('status', 'completed', completed), 'Completed'), diff --git a/app/components/ActivitySummary.js b/app/components/ActivitySummary.js index 89d8402f7..6feea77ce 100644 --- a/app/components/ActivitySummary.js +++ b/app/components/ActivitySummary.js @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import moment from 'moment'; import { View, StyleSheet } from 'react-native'; import { Button, Text, Icon } from 'native-base'; import { @@ -9,6 +8,7 @@ import { } from './core'; import { colors } from '../themes/colors'; import theme from '../themes/base-theme'; +import { formatTime } from '../services/time'; const styles = StyleSheet.create({ box: { @@ -35,14 +35,6 @@ const styles = StyleSheet.create({ }, }); -const formatTime = (timestamp) => { - const time = moment(timestamp); - if (moment().isSame(time, 'day')) { - return moment(timestamp).format('[today at] h:mm A'); - } - return moment(timestamp).format('MMMM D'); -}; - const lastCompletedString = (timestamp) => { if (!timestamp) { return 'Not yet completed'; @@ -55,7 +47,7 @@ const nextScheduledString = (activity) => { return 'Unscheduled'; } if (activity.isOverdue) { - return `Was due ${formatTime(activity.nextScheduledTimestamp)}`; + return `Due on ${formatTime(activity.lastScheduledTimestamp)}`; } return `Scheduled for ${formatTime(activity.nextScheduledTimestamp)}`; }; diff --git a/app/components/core/NotificationDot.js b/app/components/core/NotificationDot.js index b2dea3603..5a954db19 100644 --- a/app/components/core/NotificationDot.js +++ b/app/components/core/NotificationDot.js @@ -5,14 +5,14 @@ import { colors } from '../../theme'; const styles = StyleSheet.create({ container: { backgroundColor: colors.alert, - width: 20, - height: 20, + width: 15, + height: 15, // justifyContent: 'center', // alignItems: 'center', marginLeft: 12, marginTop: 6, borderRadius: 10, - elevation: 2, + elevation: 1, position: 'absolute', }, }); diff --git a/app/scenes/ActivityDetails/ActivityDetailsComponent.js b/app/scenes/ActivityDetails/ActivityDetailsComponent.js index c9b5788b9..dd5471436 100644 --- a/app/scenes/ActivityDetails/ActivityDetailsComponent.js +++ b/app/scenes/ActivityDetails/ActivityDetailsComponent.js @@ -22,6 +22,7 @@ const ActivityDetailsComponent = ({ primaryColor, }) => { const responses = responseHistory.filter((response) => { + // TODO: Update below or remove (currently unused) ActivityDetails from navigator. const responseActivityId = R.path(['meta', 'activity', '@id'], response); const formattedId = `activity/${responseActivityId}`; return activity.id === formattedId; diff --git a/app/services/network.js b/app/services/network.js index 32ccf93b5..7efdcccce 100644 --- a/app/services/network.js +++ b/app/services/network.js @@ -81,6 +81,12 @@ export const getResponses = (authToken, applet) => get( { applet }, ); +export const getSchedule = (authToken, timezone) => get( + 'schedule', + authToken, + { timezone }, +); + export const getApplets = (authToken, userId) => get( `user/applets`, authToken, diff --git a/app/services/time.js b/app/services/time.js index 8920bde2f..1b402fcc5 100644 --- a/app/services/time.js +++ b/app/services/time.js @@ -1,128 +1,64 @@ import moment from 'moment'; import * as R from 'ramda'; -export const sortMomentAr = momentAr => momentAr.sort((a, b) => { - if (a.isBefore(b)) { - return -1; - } - if (b.isBefore(a)) { - return 1; - } - return 0; -}); +const COVER_DAY = true; +const TIMED_EVENTS = true; -export const getScheduledCalendarDates = (activity, start) => { - if (R.path(['meta', 'notification', 'modeDate'], activity)) { - const calendarDayAr = activity.meta.notification.calendarDay; - return calendarDayAr.map(day => moment(day)) - .filter(day => day.isSameOrAfter(start)); - } - return []; -}; +const getStartOfInterval = R.pathOr(null, [0, 'start', 'date']); -export const getScheduledMonthDays = (activity, start, end) => { - if (R.path(['meta', 'notification', 'modeMonth'], activity)) { - const monthDayAr = activity.meta.notification.monthDay; - const index = start.clone(); - const accumulator = []; - // Step through the search period - while (index.isBefore(end)) { - if (typeof monthDayAr !== 'undefined' && monthDayAr.includes(index.date())) { - accumulator.push(index.clone()); - } - index.add(1, 'day'); - } - return accumulator; - } - return []; -}; +export const NOTIFICATION_DATETIME_FORMAT = 'YYYYMMDD HH:mm'; -export const getScheduledWeekDays = (activity, start, end) => { - if (R.path(['meta', 'notification', 'modeWeek'], activity)) { - const weekDayAr = activity.meta.notification.weekDay; - const index = start.clone(); - const accumulator = []; - // Step through the search period - while (index.isBefore(end)) { - if (typeof weekDayAr !== 'undefined' && weekDayAr.includes(index.day())) { - accumulator.push(index.clone()); - } - index.add(1, 'day'); - } - return accumulator; +export const formatTime = (timestamp) => { + const time = moment(timestamp); + if (moment().isSame(time, 'day')) { + return moment(timestamp).format('[Today at] h:mm A'); } - return []; -}; - -export const getScheduledDates = (activity, start, end) => sortMomentAr([ - ...getScheduledCalendarDates(activity, start), - ...getScheduledMonthDays(activity, start, end), - ...getScheduledWeekDays(activity, start, end), -]); - -export const getDateTimes = (dates, timeAr) => { - // If no times been set, default to 9:00 AM - const defaultTime = { time: '09:00', timeMode: 'scheduled' }; - const safeTimeAr = timeAr.length === 0 ? [defaultTime] : timeAr; - - const dateTimes = safeTimeAr.reduce((acc, time) => { - const parsedTime = moment(time.time, 'HH:mm'); - const currentDateTimes = dates.map( - date => date.clone() - .set('hour', parsedTime.get('hour')) - .set('minute', parsedTime.get('minute')) - .set('second', 0) - .set('millisecond', 0), - ); - return [...acc, ...currentDateTimes]; - }, []); - - return sortMomentAr(dateTimes); + return moment(timestamp).format('MMMM D'); }; -export const getScheduledDateTimes = (activity, start, end) => { - // console.log('activity', activity.name.en, activity); - // Get all the scheduled dates - const dates = getScheduledDates(activity, start, end); +// Generates a list of timestamps for when local notifications should be triggered, +// reminding the user to complete their activities. Notification times are set in the +// admin panel alongside the schedule for each activity. +export const getScheduledNotifications = (eventSchedule, now, notifications) => { + const dates = eventSchedule.forecast(now, COVER_DAY, 4, 0).map(d => d[2]).array(); + const dateTimes = []; + + dates.forEach((date) => { + notifications.forEach((n) => { + // TODO: handle randomized notifiation times + // i.e. if n.random is true generate notification time between n.start and n.end + const timestamp = moment(`${date[2]} ${n.start}`, NOTIFICATION_DATETIME_FORMAT); + // The dayspan library can return dates from the past when projecting next dates, + // so let's filter those out. + if (timestamp.isAfter(moment(now.date))) { + dateTimes.push(timestamp); + } + }); + }); - // Attach times to the scheduled dates (will multiply the total number of - // scheduled times by the number of scheduled times) - const times = R.pathOr([], ['meta', 'notification', 'times'], activity) - .filter(time => time.timeMode === 'scheduled'); - return getDateTimes(dates, times).filter(dateTime => dateTime.isBetween(start, end)); + return dateTimes; }; -export const getNextAndLastTimes = (activity, nowTimestamp) => { - // Get all the scheduled date/times - const start = moment(nowTimestamp).subtract(1, 'month'); - const end = moment(nowTimestamp).add(1, 'month'); - const dateTimes = getScheduledDateTimes(activity, start, end); - - // Split up the times based on before and after current time - const now = moment(nowTimestamp); - const beforeNow = dateTimes.filter(dateTime => dateTime.isBetween(start, now, null, '[]')); - const afterNow = dateTimes.filter(dateTime => dateTime.isBetween(now, end, null, '(]')); - return { - last: beforeNow.length > 0 ? beforeNow.pop().valueOf() : null, - next: afterNow.length > 0 ? afterNow[0].valueOf() : null, - }; +// Find the last event scheduled for before (or including) now. +export const getLastScheduled = (eventSchedule, now) => { + const pastSchedule = eventSchedule + .forecast(now, COVER_DAY, 0, 2, TIMED_EVENTS); + const pastDays = pastSchedule.array().filter((event) => { + // Include only events that started before now. + return moment(getStartOfInterval(event)).isBefore(now.date); + }); + const mostRecentLast = R.last(pastDays); + return getStartOfInterval(mostRecentLast); }; -export const getLastResponseTime = (activity, responses) => { - const activityResponses = responses[activity.id]; - - if (typeof activityResponses === 'undefined') { - return null; // No responses for that activity - } - - const createdTimestamp = R.path(['meta', 'responseCompleted']); - - const lastResponse = activityResponses.reduce( - (champion, challenger) => (createdTimestamp(challenger) > createdTimestamp(champion) - ? challenger - : champion), - activityResponses[0], - ); - - return createdTimestamp(lastResponse); +// Find the immediately next scheduled event. +export const getNextScheduled = (eventSchedule, now) => { + const futureSchedule = eventSchedule + .forecast(now, COVER_DAY, 1, 0, TIMED_EVENTS); + const nextDays = futureSchedule.array().filter((event) => { + // Include only events that start after now. + return moment(getStartOfInterval(event)).isAfter(now.date); + }); + const earliestNext = R.head(nextDays); + return getStartOfInterval(earliestNext); }; diff --git a/app/services/time.test.js b/app/services/time.test.js index 44a80bc0b..b394efa67 100644 --- a/app/services/time.test.js +++ b/app/services/time.test.js @@ -1,140 +1,161 @@ -/* eslint camelcase: 0 */ - import moment from 'moment'; +import * as R from 'ramda'; +import { Parse, Day } from 'dayspan'; import { - getNextAndLastTimes, - sortMomentAr, - getScheduledCalendarDates, - getScheduledMonthDays, - getScheduledWeekDays, - getDateTimes, + formatTime, + getLastScheduled, + getNextScheduled, + getScheduledNotifications, + NOTIFICATION_DATETIME_FORMAT, } from './time'; -const febraury26 = moment('2019-02-26T00:00:00'); -const march1_midnight = moment('2019-03-01T00:00:00'); -const march2_midnight = moment('2019-03-02T00:00:00'); -const march4_midnight = moment('2019-03-04T00:00:00'); -const march4_9am = moment('2019-03-04T09:00:00'); // Monday -const march4_10am = moment('2019-03-04T10:00:00'); -const march4_2pm = moment('2019-03-04T14:00:00'); -const march7 = moment('2019-03-07T00:00:00'); // Thursday -const march8_midnight = moment('2019-03-08T00:00:00'); -const march8_9am = moment('2019-03-08T09:00:00'); // Friday -const march8_10am = moment('2019-03-08T10:00:00'); -const march9_midnight = moment('2019-03-09T00:00:00'); -const march28_midnight = moment('2019-03-28T00:00:00'); - -const mockActivityWithoutTimes = { - meta: { - notification: { - advance: false, - calendarDay: [ - '2019-02-27', - '2019-03-28', - ], - modeDate: true, - modeMonth: true, - modeWeek: true, - monthDay: [ - 1, - ], - resetDate: true, - resetTime: true, - times: [], - weekDay: [ - 1, - 5, - ], - }, +const weekdaySchedule = Parse.schedule({ + dayOfWeek: [ + 1, + 2, + 3, + 4, + 5, + ], +}); + +const onceWeeklySchedule = Parse.schedule({ + dayOfWeek: [ + 3, + ], +}); + +const oneDaySchedule = Parse.schedule({ + dayOfMonth: [ + 12, + ], + month: [ + 10, + ], + year: [ + 2019, + ], +}); + +const twiceDailyNotifications = [ + { + end: null, + notifyIfIncomplete: false, + random: false, + start: '12:30', }, -}; - -const mockActivityTenAndTwo = { - meta: { - notification: { - advance: false, - calendarDay: [ - '2019-02-27', - '2019-03-28', - ], - modeDate: true, - modeMonth: true, - modeWeek: true, - monthDay: [ - 1, - ], - resetDate: true, - resetTime: true, - times: [ - { time: '10:00', timeMode: 'scheduled' }, - { time: '14:00', timeMode: 'scheduled' }, - { time: '12:00', timeMode: 'random' }, - ], - weekDay: [ - 1, - 5, - ], - }, + { + end: null, + notifyIfIncomplete: false, + random: false, + start: '18:30', }, -}; - -test('getNextAndLastTimes should have a default time of 9:00 AM', () => { - expect(getNextAndLastTimes(mockActivityWithoutTimes, march7.valueOf())) - .toEqual({ - last: march4_9am.valueOf(), - next: march8_9am.valueOf(), - }); +]; + +const notificationMoment = R.partialRight(moment, [NOTIFICATION_DATETIME_FORMAT]); + +test('schedule next - weekday schedule on a Monday', () => { + const now = Day.fromDate(new Date(2019, 10, 18, 20, 30, 0)); + const expectedNext = new Date(2019, 10, 19, 0, 0, 0); + const actualNext = getNextScheduled(weekdaySchedule, now); + expect(actualNext).toEqual(expectedNext); +}); + +test('schedule next - weekday schedule on a Friday', () => { + const now = Day.fromDate(new Date(2019, 10, 22, 20, 30, 0)); + const expectedNext = new Date(2019, 10, 25, 0, 0, 0); + const actualNext = getNextScheduled(weekdaySchedule, now); + expect(actualNext).toEqual(expectedNext); }); -test('getNextAndLastTimes should work with multiple times', () => { - expect(getNextAndLastTimes(mockActivityTenAndTwo, march7.valueOf())) - .toEqual({ - last: march4_2pm.valueOf(), - next: march8_10am.valueOf(), - }); +test('schedule next - one day schedule in the past', () => { + const now = Day.fromDate(new Date(2019, 10, 22, 20, 30, 0)); + const expectedNext = null; + const actualNext = getNextScheduled(oneDaySchedule, now); + expect(actualNext).toEqual(expectedNext); }); -test('sortMomentAr', () => { - const momentAr = [ - march7, - march8_10am, - march4_2pm, - march4_9am, - march8_9am, +test('schedule last - weekday schedule on a Monday', () => { + const now = Day.fromDate(new Date(2019, 10, 25, 20, 30, 0)); + const expectedLast = new Date(2019, 10, 25, 0, 0, 0); + const actualLast = getLastScheduled(weekdaySchedule, now); + expect(actualLast).toEqual(expectedLast); +}); + +test('schedule last - once weekly schedule on a Monday', () => { + const now = Day.fromDate(new Date(2019, 10, 25, 20, 30, 0)); + const expectedLast = new Date(2019, 10, 20, 0, 0, 0); + const actualLast = getLastScheduled(onceWeeklySchedule, now); + expect(actualLast).toEqual(expectedLast); +}); + +test('schedule last - one day schedule in the future', () => { + const now = Day.fromDate(new Date(2019, 9, 20, 20, 30, 0)); + const expectedLast = null; + const actualLast = getLastScheduled(oneDaySchedule, now); + expect(actualLast).toEqual(expectedLast); +}); + +test('schedule notifications twice daily on a one day schedule', () => { + const now = Day.fromDate(new Date(2019, 9, 20, 20, 30, 0)); + const expectedNotifications = [ + notificationMoment('20191112 12:30'), + notificationMoment('20191112 18:30'), ]; + const actualNotifications = getScheduledNotifications(oneDaySchedule, now, twiceDailyNotifications); + expect(actualNotifications).toEqual(expectedNotifications); +}); - expect(JSON.stringify(sortMomentAr(momentAr))) - .toEqual(JSON.stringify([ - march4_9am, - march4_2pm, - march7, - march8_9am, - march8_10am, - ])); +test('schedule notifications twice daily on a one day schedule', () => { + const now = Day.fromDate(new Date(2019, 9, 20, 20, 30, 0)); + const expectedNotifications = [ + notificationMoment('20191112 12:30'), // notifications for next and only day + notificationMoment('20191112 18:30'), + ]; + const actualNotifications = getScheduledNotifications(oneDaySchedule, now, twiceDailyNotifications); + expect(actualNotifications).toEqual(expectedNotifications); }); -test('getScheduledCalendarDates', () => { - expect(JSON.stringify(getScheduledCalendarDates(mockActivityWithoutTimes, march7))) - .toEqual(JSON.stringify([march28_midnight])); +test('schedule notifications twice daily on a weekday schedule starting on the weekend', () => { + const now = Day.fromDate(new Date(2019, 10, 16, 0, 0, 0)); + const expectedNotifications = [ + notificationMoment('20191118 12:30'), // notifications for next 4 days + notificationMoment('20191118 18:30'), + notificationMoment('20191119 12:30'), + notificationMoment('20191119 18:30'), + notificationMoment('20191120 12:30'), + notificationMoment('20191120 18:30'), + notificationMoment('20191121 12:30'), + notificationMoment('20191121 18:30'), + ]; + const actualNotifications = getScheduledNotifications(weekdaySchedule, now, twiceDailyNotifications); + expect(actualNotifications).toEqual(expectedNotifications); }); -test('getScheduledMonthDays', () => { - expect(JSON.stringify(getScheduledMonthDays(mockActivityWithoutTimes, febraury26, march7))) - .toEqual(JSON.stringify([march1_midnight])); +test('schedule notifications twice daily on a weekday schedule starting on a Monday', () => { + const now = Day.fromDate(new Date(2019, 10, 18, 0, 0, 0)); + const expectedNotifications = [ + notificationMoment('20191118 12:30'), // notifications for today + notificationMoment('20191118 18:30'), + notificationMoment('20191119 12:30'), // notifications for next 4 days + notificationMoment('20191119 18:30'), + notificationMoment('20191120 12:30'), + notificationMoment('20191120 18:30'), + notificationMoment('20191121 12:30'), + notificationMoment('20191121 18:30'), + notificationMoment('20191122 12:30'), + notificationMoment('20191122 18:30'), + ]; + const actualNotifications = getScheduledNotifications(weekdaySchedule, now, twiceDailyNotifications); + expect(actualNotifications).toEqual(expectedNotifications); }); -test('getScheduledWeekDays', () => { - expect(JSON.stringify(getScheduledWeekDays( - mockActivityWithoutTimes, - march2_midnight, - march9_midnight, - ))) - .toEqual(JSON.stringify([march4_midnight, march8_midnight])); +test('formats time for today', () => { + const earlierToday = moment().subtract(2, 'minutes'); + expect(formatTime(earlierToday)).toEqual(`Today at ${moment(earlierToday).format('h:mm A')}`); }); -test('getDateTimes', () => { - const dates = [march4_midnight, march8_midnight]; - const times = [{ time: '10:00', timeMode: 'scheduled' }]; - expect(JSON.stringify(getDateTimes(dates, times))) - .toEqual(JSON.stringify([march4_10am, march8_10am])); +test('formats time for yesterday', () => { + const yesterday = moment().subtract(1, 'day'); + expect(formatTime(yesterday)).toEqual(moment(yesterday).format('MMMM D')); }); diff --git a/app/state/applets/applets.selectors.js b/app/state/applets/applets.selectors.js index e0296ed6c..76379af16 100644 --- a/app/state/applets/applets.selectors.js +++ b/app/state/applets/applets.selectors.js @@ -2,66 +2,70 @@ import { createSelector } from 'reselect'; import * as R from 'ramda'; import { Parse, Day } from 'dayspan'; import moment from 'moment'; -import { getLastResponseTime, getNextAndLastTimes } from '../../services/time'; -import { responsesGroupedByActivitySelector } from '../responses/responses.selectors'; - -// import console = require('console'); +import { + getLastScheduled, + getNextScheduled, + getScheduledNotifications, +} from '../../services/time'; +import { responseScheduleSelector } from '../responses/responses.selectors'; export const dateParser = (schedule) => { - // output is an object indexed by URIs. const output = {}; - schedule.events.map((e) => { - const uri = e.data.URI; + schedule.events.forEach((e) => { + const uri = e.data.URI; if (!output[uri]) { - output[uri] = []; + output[uri] = { + notificationDateTimes: [], + }; } - let { notifications } = e.data; - notifications = notifications || []; - // TODO: e.data might have some flag saying its relative to - // the first response. update the schedule here before parsing it. - // this might be kind of hard. const eventSchedule = Parse.schedule(e.schedule); - const today = Day.fromDate(new Date()); - const dates = eventSchedule.forecast(today, false, 4, 0).map(d => d[2]).array(); - const dateTimes = []; - // create a list of datetimes - // for each date in dates and - // for each notification in notifications - // TODO: handle random notification times (if n.random is true, - // then generate a time between n.start and n.end) - dates.map(date => notifications.map(n => dateTimes.push(moment(`${date[2]} ${n.start}`, 'YYYYMMDD HH:mm')))); - // TODO: only append unique datetimes - output[uri] = output[uri].concat(dateTimes); - return 0; + const now = Day.fromDate(new Date()); + + const lastScheduled = getLastScheduled(eventSchedule, now); + const nextScheduled = getNextScheduled(eventSchedule, now); + + const notifications = R.pathOr([], ['data', 'notifications'], e); + const dateTimes = getScheduledNotifications(eventSchedule, now, notifications); + + output[uri] = { + lastScheduledResponse: output[uri].lastScheduledResponse && lastScheduled + ? moment.max(moment(output[uri].lastScheduledResponse), moment(lastScheduled)) + : lastScheduled, + nextScheduledResponse: output[uri].nextScheduledResponse && nextScheduled + ? moment.min(moment(output[uri].nextScheduledResponse), moment(nextScheduled)) + : nextScheduled, + + // TODO: only append unique datetimes when multiple events scheduled for same activity/URI + notificationDateTimes: output[uri].notificationDateTimes.concat(dateTimes), + }; }); + return output; }; // Attach some info to each activity export const appletsSelector = createSelector( R.path(['applets', 'applets']), - responsesGroupedByActivitySelector, - (applets, responses) => applets.map((applet) => { + responseScheduleSelector, + (applets, responseSchedule) => applets.map((applet) => { + let scheduledDateTimesByActivity = {}; + // applet.schedule, if defined, has an events key. // events is a list of objects. // the events[idx].data.URI points to the specific activity's schema. - - let notificationDateTimesByActivity = {}; if (applet.schedule) { - notificationDateTimesByActivity = dateParser(applet.schedule); + scheduledDateTimesByActivity = dateParser(applet.schedule); } const extraInfoActivities = applet.activities.map((act) => { - const now = Date.now(); - const { last, next } = getNextAndLastTimes(act, now); - const lastResponse = getLastResponseTime(act, responses); - // add in our parsed notifications here. + const scheduledDateTimes = scheduledDateTimesByActivity[act.schema]; - // eslint-disable-next-line - act.notification = notificationDateTimesByActivity[act.schema]; + const nextScheduled = R.pathOr(null, ['nextScheduledResponse'], scheduledDateTimes); + const lastScheduled = R.pathOr(null, ['lastScheduledResponse'], scheduledDateTimes); + const lastResponse = R.path([applet.id, act.id, 'lastResponse'], responseSchedule); return { ...act, @@ -70,12 +74,16 @@ export const appletsSelector = createSelector( appletName: applet.name, appletSchema: applet.schema, appletSchemaVersion: applet.schemaVersion, - lastScheduledTimestamp: last, + lastScheduledTimestamp: lastScheduled, lastResponseTimestamp: lastResponse, - nextScheduledTimestamp: next, - isOverdue: last && lastResponse < last, + nextScheduledTimestamp: nextScheduled, + isOverdue: lastScheduled && moment(lastResponse) < moment(lastScheduled), + + // also add in our parsed notifications... + notification: R.prop('notificationDateTimes', scheduledDateTimes), }; }); + return { ...applet, activities: extraInfoActivities, diff --git a/app/state/applets/applets.thunks.js b/app/state/applets/applets.thunks.js index 8c9582cbd..9f978a245 100644 --- a/app/state/applets/applets.thunks.js +++ b/app/state/applets/applets.thunks.js @@ -22,7 +22,6 @@ import { setInvites, saveAppletResponseData, } from './applets.actions'; -// eslint-disable-next-line import { sync } from '../app/app.thunks'; import { transformApplet } from '../../models/json-ld'; @@ -31,7 +30,6 @@ export const scheduleAndSetNotifications = () => (dispatch, getState) => { const activities = activitiesSelector(state); // This call schedules the notifications and returns a list of scheduled notifications const updatedNotifications = scheduleNotifications(activities); - console.log('dispatching set notifications', activities, updatedNotifications); dispatch(setNotifications(updatedNotifications)); }; diff --git a/app/state/responses/responses.thunks.js b/app/state/responses/responses.thunks.js index 40e52de7b..f674ccb0a 100644 --- a/app/state/responses/responses.thunks.js +++ b/app/state/responses/responses.thunks.js @@ -1,6 +1,8 @@ import * as R from 'ramda'; import { Alert } from 'react-native'; import { Actions } from 'react-native-router-flux'; +import * as RNLocalize from 'react-native-localize'; +import { getSchedule } from '../../services/network'; import { downloadAllResponses, uploadResponseQueue } from '../../services/api'; import { cleanFiles } from '../../services/file'; import { prepareResponseForUpload } from '../../models/response'; @@ -19,6 +21,7 @@ import { addToUploadQueue, shiftUploadQueue, setCurrentScreen, + setSchedule, } from './responses.actions'; import { setCurrentActivity, @@ -88,15 +91,11 @@ export const startResponse = activity => (dispatch, getState) => { } }; - -/** - * TODO: the below thunk isn't that useful. Instead, - * we want to download data from the last 7 days. - */ export const downloadResponses = () => (dispatch, getState) => { const state = getState(); const authToken = authTokenSelector(state); const applets = appletsSelector(state); + dispatch(setDownloadingResponses(true)); downloadAllResponses(authToken, applets, (downloaded, total) => { dispatch(setResponsesDownloadProgress(downloaded, total)); @@ -109,6 +108,12 @@ export const downloadResponses = () => (dispatch, getState) => { }).finally(() => { dispatch(setDownloadingResponses(false)); }); + + const timezone = RNLocalize.getTimeZone(); + getSchedule(authToken, timezone) + .then((schedule) => { + dispatch(setSchedule(schedule)); + }); }; export const startUploadQueue = () => (dispatch, getState) => { diff --git a/ios/MDCApp.xcodeproj/project.pbxproj b/ios/MDCApp.xcodeproj/project.pbxproj index 7e27d9af0..6ee4f45e1 100644 --- a/ios/MDCApp.xcodeproj/project.pbxproj +++ b/ios/MDCApp.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ EA96CBA8794A4C45BE766879 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E67D95072BF64F618F605B9B /* Ionicons.ttf */; }; F4FA666142214DC98AB06CA1 /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5665996285D148F6B9F92DF6 /* libRNDeviceInfo.a */; }; FC6914426D1A47D585609705 /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DCDEE04E7F74A8E989DAABB /* libRNVectorIcons.a */; }; + 5CB85CD466C24DA8B4CE3CF6 /* libRNLocalize.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E84564DB49714A008DB1A97C /* libRNLocalize.a */; }; + BBD83B138B8044DFADA12945 /* libRNLocalize-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 21D8A3188CBE4FC083A66C5E /* libRNLocalize-tvOS.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -598,6 +600,9 @@ ED9DAE77C19146CF8F900A3F /* RNAudio.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNAudio.xcodeproj; path = "../node_modules/react-native-audio/ios/RNAudio.xcodeproj"; sourceTree = ""; }; F995F616F3FC4DD0B246C5DE /* RNFetchBlob.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNFetchBlob.xcodeproj; path = "../node_modules/rn-fetch-blob/ios/RNFetchBlob.xcodeproj"; sourceTree = ""; }; FEAA2F7D382D448C895FFC9A /* FontAwesome5_Brands.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Brands.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"; sourceTree = ""; }; + 58A6A7C6A15F4859A53BBF34 /* RNLocalize.xcodeproj */ = {isa = PBXFileReference; name = "RNLocalize.xcodeproj"; path = "../node_modules/react-native-localize/ios/RNLocalize.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + E84564DB49714A008DB1A97C /* libRNLocalize.a */ = {isa = PBXFileReference; name = "libRNLocalize.a"; path = "libRNLocalize.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; + 21D8A3188CBE4FC083A66C5E /* libRNLocalize-tvOS.a */ = {isa = PBXFileReference; name = "libRNLocalize-tvOS.a"; path = "libRNLocalize-tvOS.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -642,6 +647,7 @@ 57C5D1FACE404B2DA8954FE8 /* libReactNativePermissions.a in Frameworks */, E7A158CC419E4F0480F982FE /* libRNCNetInfo.a in Frameworks */, 64126CB9F78E4C2BB0390F3C /* libRNCSlider.a in Frameworks */, + 5CB85CD466C24DA8B4CE3CF6 /* libRNLocalize.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -660,6 +666,7 @@ 759CE0193CCD4BD9AFFE3987 /* libRNDeviceInfo-tvOS.a in Frameworks */, 2D88D21249504EE5B8538216 /* libRNVectorIcons-tvOS.a in Frameworks */, AD8854BEC2C64215B92A4781 /* libRNCNetInfo-tvOS.a in Frameworks */, + BBD83B138B8044DFADA12945 /* libRNLocalize-tvOS.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -972,6 +979,7 @@ E220B4C7A88047E8BA61A0EC /* ReactNativePermissions.xcodeproj */, 185BD87C944C4876B66A4296 /* RNCNetInfo.xcodeproj */, 5B4449E923CC41D6990FCCA7 /* RNCSlider.xcodeproj */, + 58A6A7C6A15F4859A53BBF34 /* RNLocalize.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -1916,6 +1924,7 @@ "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/@react-native-community/netinfo/ios", "$(SRCROOT)/../node_modules/@react-native-community/slider/ios", + "$(SRCROOT)/../node_modules/react-native-localize/ios", ); INFOPLIST_FILE = MDCAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1929,6 +1938,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1964,6 +1975,7 @@ "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/@react-native-community/netinfo/ios", "$(SRCROOT)/../node_modules/@react-native-community/slider/ios", + "$(SRCROOT)/../node_modules/react-native-localize/ios", ); INFOPLIST_FILE = MDCAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1977,6 +1989,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -2017,6 +2031,7 @@ "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/@react-native-community/netinfo/ios", "$(SRCROOT)/../node_modules/@react-native-community/slider/ios", + "$(SRCROOT)/../node_modules/react-native-localize/ios", ); INFOPLIST_FILE = MDCApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2062,6 +2077,7 @@ "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/@react-native-community/netinfo/ios", "$(SRCROOT)/../node_modules/@react-native-community/slider/ios", + "$(SRCROOT)/../node_modules/react-native-localize/ios", ); INFOPLIST_FILE = MDCApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -2109,6 +2125,7 @@ "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/@react-native-community/netinfo/ios", "$(SRCROOT)/../node_modules/@react-native-community/slider/ios", + "$(SRCROOT)/../node_modules/react-native-localize/ios", ); INFOPLIST_FILE = "MDCApp-tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -2121,6 +2138,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -2164,6 +2183,7 @@ "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/@react-native-community/netinfo/ios", "$(SRCROOT)/../node_modules/@react-native-community/slider/ios", + "$(SRCROOT)/../node_modules/react-native-localize/ios", ); INFOPLIST_FILE = "MDCApp-tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -2176,6 +2196,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -2211,6 +2233,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.MDCApp-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2242,6 +2266,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.MDCApp-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/package.json b/package.json index 9f37bd27a..37a384381 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-native-device-info": "^5.0.1", "react-native-image-picker": "^0.26.7", "react-native-img-cache": "1.6.0", + "react-native-localize": "^1.3.1", "react-native-markdown-view": "https://github.com/shnizzedy/react-native-markdown-view.git", "react-native-permissions": "^1.1.1", "react-native-progress": "^3.4.0", diff --git a/yarn.lock b/yarn.lock index 352436e36..ae5e44e3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6059,6 +6059,11 @@ react-native-keyboard-aware-scroll-view@0.5.0: prop-types "^15.6.0" react-native-iphone-x-helper "^1.0.1" +react-native-localize@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-1.3.1.tgz#d0b7046acd4214ac2bcb61102317374351400c76" + integrity sha512-Y3LzTHyrgsIsDYvjWSRguARBKjiLaahcbJg663ZqP1Tcpan4LYn/f3iusM+Oh6qYvClnlo9AlBkLdCZbWwe7Tw== + "react-native-markdown-view@https://github.com/shnizzedy/react-native-markdown-view.git": version "1.1.4" resolved "https://github.com/shnizzedy/react-native-markdown-view.git#6d9524edd0f83da0ddd3d7426d93bcc96d6a2870" From dc52811f727215fda166c20f98d74e5be76d6097 Mon Sep 17 00:00:00 2001 From: Adam Odziemkowski Date: Wed, 20 Nov 2019 14:33:46 -0500 Subject: [PATCH 3/3] Bump version to 0.9.1 --- CHANGELOG.md | 4 ++++ README.md | 2 +- android/app/build.gradle | 4 ++-- ios/MDCApp/Info.plist | 4 ++-- package.json | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb050fdd..61c920af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ======= +## [0.9.1] - 2019-11-20 +### Fixed +- Scheduling of activities + ## [0.8.10] - 2019-11-13 ### Updated - :lock: :apple: :books: User privacy descriptions diff --git a/README.md b/README.md index 8b6f30482..58bec54a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MindLogger 0.8.10 +# MindLogger 0.9.1 _Note: v0.1 is deprecated as of June 12, 2019._ diff --git a/android/app/build.gradle b/android/app/build.gradle index 72ffe7039..892b2ebd0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -101,8 +101,8 @@ android { applicationId "lab.childmindinstitute.data" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 93 - versionName "0.8.10" + versionCode 94 + versionName "0.9.1" ndk { abiFilters "arm64-v8a", "x86_64", "armeabi-v7a", "x86" } diff --git a/ios/MDCApp/Info.plist b/ios/MDCApp/Info.plist index f932c78a1..ffc6aa7a1 100644 --- a/ios/MDCApp/Info.plist +++ b/ios/MDCApp/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.8.10 + 0.9.1 CFBundleSignature ???? CFBundleVersion - 93 + 94 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/package.json b/package.json index 37a384381..b5e74775c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "MindLogger", - "version": "0.8.10", + "version": "0.9.1", "private": true, "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start",