diff --git a/www/__tests__/commHelper.test.ts b/www/__tests__/commHelper.test.ts index 8bc52a408..d7018abb5 100644 --- a/www/__tests__/commHelper.test.ts +++ b/www/__tests__/commHelper.test.ts @@ -1,5 +1,5 @@ import { mockLogger } from '../__mocks__/globalMocks'; -import { fetchUrlCached } from '../js/commHelper'; +import { fetchUrlCached } from '../js/services/commHelper'; mockLogger(); diff --git a/www/__tests__/confirmHelper.test.ts b/www/__tests__/confirmHelper.test.ts index 17c23a972..6dd94b808 100644 --- a/www/__tests__/confirmHelper.test.ts +++ b/www/__tests__/confirmHelper.test.ts @@ -1,5 +1,5 @@ import { mockLogger } from '../__mocks__/globalMocks'; -import * as CommHelper from '../js/commHelper'; +import * as CommHelper from '../js/services/commHelper'; import { baseLabelInputDetails, getLabelInputDetails, diff --git a/www/__tests__/storeDeviceSettings.test.ts b/www/__tests__/storeDeviceSettings.test.ts index 3cd6f8319..4bccbc0af 100644 --- a/www/__tests__/storeDeviceSettings.test.ts +++ b/www/__tests__/storeDeviceSettings.test.ts @@ -1,6 +1,6 @@ import { readConsentState, markConsented } from '../js/splash/startprefs'; import { storageClear } from '../js/plugin/storage'; -import { getUser } from '../js/commHelper'; +import { getUser } from '../js/services/commHelper'; import { initStoreDeviceSettings, teardownDeviceSettings } from '../js/splash/storeDeviceSettings'; import { mockBEMDataCollection, diff --git a/www/__tests__/unifiedDataLoader.test.ts b/www/__tests__/unifiedDataLoader.test.ts new file mode 100644 index 000000000..57b1023da --- /dev/null +++ b/www/__tests__/unifiedDataLoader.test.ts @@ -0,0 +1,150 @@ +import { mockLogger } from '../__mocks__/globalMocks'; +import { removeDup, combinedPromises } from '../js/services/unifiedDataLoader'; +import { ServerData } from '../js/types/serverData'; + +mockLogger(); + +const testOne: ServerData = { + data: '', + metadata: { + key: '', + platform: '', + write_ts: 1, // the only value checked by removeDup + time_zone: '', + write_fmt_time: '', + write_local_dt: null, + }, +}; + +const testTwo = JSON.parse(JSON.stringify(testOne)); +testTwo.metadata.write_ts = 2; +const testThree = JSON.parse(JSON.stringify(testOne)); +testThree.metadata.write_ts = 3; +const testFour = JSON.parse(JSON.stringify(testOne)); +testFour.metadata.write_ts = 4; + +describe('removeDup can', () => { + it('work with an empty array', () => { + expect(removeDup([])).toEqual([]); + }); + + it('work with an array of len 1', () => { + expect(removeDup([testOne])).toEqual([testOne]); + }); + + it('work with an array of len >=1', () => { + expect(removeDup([testOne, testTwo])).toEqual([testOne, testTwo]); + expect(removeDup([testOne, testOne])).toEqual([testOne]); + expect(removeDup([testOne, testTwo, testThree])).toEqual([testOne, testTwo, testThree]); + expect(removeDup([testOne, testOne, testThree])).toEqual([testOne, testThree]); + expect(removeDup([testOne, testOne, testOne])).toEqual([testOne]); + }); +}); + +// combinedPromises tests +const promiseGenerator = (values: Array>) => { + return Promise.resolve(values); +}; +const badPromiseGenerator = (input: string) => { + return Promise.reject(input); +}; + +it('throws an error on an empty input', async () => { + expect(() => { + combinedPromises([], removeDup); + }).toThrow(); +}); + +it('catches when all promises fails', async () => { + expect(combinedPromises([badPromiseGenerator('')], removeDup)).rejects.toEqual(['']); + expect( + combinedPromises([badPromiseGenerator('bad'), badPromiseGenerator('promise')], removeDup), + ).rejects.toEqual(['bad', 'promise']); + expect( + combinedPromises( + [badPromiseGenerator('very'), badPromiseGenerator('bad'), badPromiseGenerator('promise')], + removeDup, + ), + ).rejects.toEqual(['very', 'bad', 'promise']); + + expect( + combinedPromises([badPromiseGenerator('bad'), promiseGenerator([testOne])], removeDup), + ).resolves.toEqual([testOne]); + expect( + combinedPromises([promiseGenerator([testOne]), badPromiseGenerator('bad')], removeDup), + ).resolves.toEqual([testOne]); +}); + +it('work with arrays of len 1', async () => { + const promiseArrayOne = [promiseGenerator([testOne])]; + const promiseArrayTwo = [promiseGenerator([testOne, testTwo])]; + const testResultOne = await combinedPromises(promiseArrayOne, removeDup); + const testResultTwo = await combinedPromises(promiseArrayTwo, removeDup); + + expect(testResultOne).toEqual([testOne]); + expect(testResultTwo).toEqual([testOne, testTwo]); +}); + +it('works with arrays of len 2', async () => { + const promiseArrayOne = [promiseGenerator([testOne]), promiseGenerator([testTwo])]; + const promiseArrayTwo = [promiseGenerator([testOne, testTwo]), promiseGenerator([testThree])]; + const promiseArrayThree = [promiseGenerator([testOne]), promiseGenerator([testTwo, testThree])]; + const promiseArrayFour = [ + promiseGenerator([testOne, testTwo]), + promiseGenerator([testThree, testFour]), + ]; + const promiseArrayFive = [ + promiseGenerator([testOne, testTwo]), + promiseGenerator([testTwo, testThree]), + ]; + + const testResultOne = await combinedPromises(promiseArrayOne, removeDup); + const testResultTwo = await combinedPromises(promiseArrayTwo, removeDup); + const testResultThree = await combinedPromises(promiseArrayThree, removeDup); + const testResultFour = await combinedPromises(promiseArrayFour, removeDup); + const testResultFive = await combinedPromises(promiseArrayFive, removeDup); + + expect(testResultOne).toEqual([testOne, testTwo]); + expect(testResultTwo).toEqual([testOne, testTwo, testThree]); + expect(testResultThree).toEqual([testOne, testTwo, testThree]); + expect(testResultFour).toEqual([testOne, testTwo, testThree, testFour]); + expect(testResultFive).toEqual([testOne, testTwo, testThree]); +}); + +it('works with arrays of len >= 2', async () => { + const promiseArrayOne = [ + promiseGenerator([testOne]), + promiseGenerator([testTwo]), + promiseGenerator([testThree]), + ]; + const promiseArrayTwo = [ + promiseGenerator([testOne]), + promiseGenerator([testTwo]), + promiseGenerator([testTwo]), + ]; + const promiseArrayThree = [ + promiseGenerator([testOne]), + promiseGenerator([testTwo]), + promiseGenerator([testThree, testFour]), + ]; + const promiseArrayFour = [ + promiseGenerator([testOne]), + promiseGenerator([testTwo, testThree]), + promiseGenerator([testFour]), + ]; + + const testResultOne = await combinedPromises(promiseArrayOne, removeDup); + const testResultTwo = await combinedPromises(promiseArrayTwo, removeDup); + const testResultThree = await combinedPromises(promiseArrayThree, removeDup); + const testResultFour = await combinedPromises(promiseArrayFour, removeDup); + + expect(testResultOne).toEqual([testOne, testTwo, testThree]); + expect(testResultTwo).toEqual([testOne, testTwo]); + expect(testResultThree).toEqual([testOne, testTwo, testThree, testFour]); + expect(testResultFour).toEqual([testOne, testTwo, testThree, testFour]); +}); + +/* + TO-DO: Once getRawEnteries can be tested via end-to-end testing, we will be able to + test getUnifiedDataForInterval as well. +*/ diff --git a/www/img/adam.jpg b/www/img/adam.jpg deleted file mode 100644 index 5a5d37ccc..000000000 Binary files a/www/img/adam.jpg and /dev/null differ diff --git a/www/img/avatar_1.png b/www/img/avatar_1.png deleted file mode 100644 index d4ff83f76..000000000 Binary files a/www/img/avatar_1.png and /dev/null differ diff --git a/www/img/banana.png b/www/img/banana.png deleted file mode 100644 index 0a7beedf6..000000000 Binary files a/www/img/banana.png and /dev/null differ diff --git a/www/img/ben.png b/www/img/ben.png deleted file mode 100644 index 374d32ef1..000000000 Binary files a/www/img/ben.png and /dev/null differ diff --git a/www/img/cookie.png b/www/img/cookie.png deleted file mode 100644 index ec7f5a5c7..000000000 Binary files a/www/img/cookie.png and /dev/null differ diff --git a/www/img/ic_header_gem.png b/www/img/ic_header_gem.png deleted file mode 100644 index 30f8b4dea..000000000 Binary files a/www/img/ic_header_gem.png and /dev/null differ diff --git a/www/img/ic_header_gold.png b/www/img/ic_header_gold.png deleted file mode 100644 index c9fd24d1b..000000000 Binary files a/www/img/ic_header_gold.png and /dev/null differ diff --git a/www/img/ic_header_healer.png b/www/img/ic_header_healer.png deleted file mode 100644 index c52dc41eb..000000000 Binary files a/www/img/ic_header_healer.png and /dev/null differ diff --git a/www/img/ic_header_mage.png b/www/img/ic_header_mage.png deleted file mode 100644 index 3395fd5fc..000000000 Binary files a/www/img/ic_header_mage.png and /dev/null differ diff --git a/www/img/ic_header_rogue.png b/www/img/ic_header_rogue.png deleted file mode 100644 index 7ac2929c7..000000000 Binary files a/www/img/ic_header_rogue.png and /dev/null differ diff --git a/www/img/ic_header_silver.png b/www/img/ic_header_silver.png deleted file mode 100644 index a07fc2581..000000000 Binary files a/www/img/ic_header_silver.png and /dev/null differ diff --git a/www/img/ic_header_warrior.png b/www/img/ic_header_warrior.png deleted file mode 100644 index 0b76436de..000000000 Binary files a/www/img/ic_header_warrior.png and /dev/null differ diff --git a/www/img/ic_navigation_black_24dp.png b/www/img/ic_navigation_black_24dp.png deleted file mode 100644 index 760fcd193..000000000 Binary files a/www/img/ic_navigation_black_24dp.png and /dev/null differ diff --git a/www/img/icecream.png b/www/img/icecream.png deleted file mode 100644 index 5a4f46436..000000000 Binary files a/www/img/icecream.png and /dev/null differ diff --git a/www/img/intro/splash_screen_logo.png b/www/img/intro/splash_screen_logo.png deleted file mode 100644 index ffabdd377..000000000 Binary files a/www/img/intro/splash_screen_logo.png and /dev/null differ diff --git a/www/img/ionic.png b/www/img/ionic.png deleted file mode 100644 index 21c7f3759..000000000 Binary files a/www/img/ionic.png and /dev/null differ diff --git a/www/img/max.png b/www/img/max.png deleted file mode 100644 index a4ab62b2f..000000000 Binary files a/www/img/max.png and /dev/null differ diff --git a/www/img/mike.png b/www/img/mike.png deleted file mode 100644 index e9abd4ddd..000000000 Binary files a/www/img/mike.png and /dev/null differ diff --git a/www/img/minus.gif b/www/img/minus.gif deleted file mode 100644 index 0115810b9..000000000 Binary files a/www/img/minus.gif and /dev/null differ diff --git a/www/img/nileredsea_126b5740_small.png b/www/img/nileredsea_126b5740_small.png deleted file mode 100644 index a1e11ca24..000000000 Binary files a/www/img/nileredsea_126b5740_small.png and /dev/null differ diff --git a/www/img/pacman.gif b/www/img/pacman.gif deleted file mode 100644 index 8201c5d6e..000000000 Binary files a/www/img/pacman.gif and /dev/null differ diff --git a/www/img/perry.png b/www/img/perry.png deleted file mode 100644 index 2ab43ffbd..000000000 Binary files a/www/img/perry.png and /dev/null differ diff --git a/www/img/plus.gif b/www/img/plus.gif deleted file mode 100644 index 6879c8743..000000000 Binary files a/www/img/plus.gif and /dev/null differ diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index 9c28958ac..eb709c16c 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -1,7 +1,6 @@ import i18next from 'i18next'; import { displayError, logDebug, logWarn } from '../plugin/logger'; -import { getAngularService } from '../angular-react-helper'; -import { fetchUrlCached } from '../commHelper'; +import { fetchUrlCached } from '../services/commHelper'; import { storageClear, storageGet, storageSet } from '../plugin/storage'; export const CONFIG_PHONE_UI = 'config/app_ui_config'; diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index 7fdf3fa37..b26d5e85a 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -9,7 +9,7 @@ import SettingRow from './SettingRow'; import AlertBar from './AlertBar'; import moment from 'moment'; import { addStatEvent, statKeys } from '../plugin/clientStats'; -import { updateUser } from '../commHelper'; +import { updateUser } from '../services/commHelper'; /* * BEGIN: Simple read/write wrappers diff --git a/www/js/controllers.js b/www/js/controllers.js index 16f962c8d..e5ab2749e 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -70,29 +70,4 @@ angular }, ); console.log('SplashCtrl invoke finished'); - }) - - .controller('ChatsCtrl', function ($scope, Chats) { - // With the new view caching in Ionic, Controllers are only called - // when they are recreated or on app start, instead of every page change. - // To listen for when this page is active (for example, to refresh data), - // listen for the $ionicView.enter event: - // - //$scope.$on('$ionicView.enter', function(e) { - //}); - - $scope.chats = Chats.all(); - $scope.remove = function (chat) { - Chats.remove(chat); - }; - }) - - .controller('ChatDetailCtrl', function ($scope, $stateParams, Chats) { - $scope.chat = Chats.get($stateParams.chatId); - }) - - .controller('AccountCtrl', function ($scope) { - $scope.settings = { - enableFriends: true, - }; }); diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 9556f0ed4..f8b222d6b 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -27,7 +27,7 @@ import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHe import { getLabelOptions } from '../survey/multilabel/confirmHelper'; import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { useTheme } from 'react-native-paper'; -import { getPipelineRangeTs } from '../commHelper'; +import { getPipelineRangeTs } from '../services/commHelper'; import { mapInputsToTimelineEntries } from '../survey/inputMatcher'; import { configuredFilters as multilabelConfiguredFilters } from '../survey/multilabel/infinite_scroll_filters'; import { configuredFilters as enketoConfiguredFilters } from '../survey/enketo/infinite_scroll_filters'; diff --git a/www/js/diary/services.js b/www/js/diary/services.js index a1b238ef8..c9bcae7c7 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -2,22 +2,14 @@ import angular from 'angular'; import { getConfig } from '../config/dynamicConfig'; -import { getRawEntries } from '../commHelper'; +import { getRawEntries } from '../services/commHelper'; +import { getUnifiedDataForInterval } from '../services/unifiedDataLoader'; angular .module('emission.main.diary.services', ['emission.plugin.logger', 'emission.services']) .factory( 'Timeline', - function ( - $http, - $ionicLoading, - $ionicPlatform, - $window, - $rootScope, - UnifiedDataLoader, - Logger, - $injector, - ) { + function ($http, $ionicLoading, $ionicPlatform, $window, $rootScope, Logger, $injector) { var timeline = {}; // corresponds to the old $scope.data. Contains all state for the current // day, including the indication of the current day @@ -256,64 +248,65 @@ angular ' -> ' + moment.unix(tripEndTransition.data.ts).toString(), ); - return UnifiedDataLoader.getUnifiedSensorDataForInterval( - 'background/filtered_location', - tq, - ).then(function (locationList) { - if (locationList.length == 0) { - return undefined; - } - var sortedLocationList = locationList.sort(tsEntrySort); - var retainInRange = function (loc) { - return ( - tripStartTransition.data.ts <= loc.data.ts && loc.data.ts <= tripEndTransition.data.ts - ); - }; + const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; + return getUnifiedDataForInterval('background/filtered_location', tq, getMethod).then( + function (locationList) { + if (locationList.length == 0) { + return undefined; + } + var sortedLocationList = locationList.sort(tsEntrySort); + var retainInRange = function (loc) { + return ( + tripStartTransition.data.ts <= loc.data.ts && + loc.data.ts <= tripEndTransition.data.ts + ); + }; - var filteredLocationList = sortedLocationList.filter(retainInRange); + var filteredLocationList = sortedLocationList.filter(retainInRange); - // Fix for https://github.com/e-mission/e-mission-docs/issues/417 - if (filteredLocationList.length == 0) { - return undefined; - } + // Fix for https://github.com/e-mission/e-mission-docs/issues/417 + if (filteredLocationList.length == 0) { + return undefined; + } - var tripStartPoint = filteredLocationList[0]; - var tripEndPoint = filteredLocationList[filteredLocationList.length - 1]; - Logger.log( - 'tripStartPoint = ' + - JSON.stringify(tripStartPoint) + - 'tripEndPoint = ' + - JSON.stringify(tripEndPoint), - ); - // if we get a list but our start and end are undefined - // let's print out the complete original list to get a clue - // this should help with debugging - // https://github.com/e-mission/e-mission-docs/issues/417 - // if it ever occurs again - if (angular.isUndefined(tripStartPoint) || angular.isUndefined(tripEndPoint)) { - Logger.log('BUG 417 check: locationList = ' + JSON.stringify(locationList)); + var tripStartPoint = filteredLocationList[0]; + var tripEndPoint = filteredLocationList[filteredLocationList.length - 1]; Logger.log( - 'transitions: start = ' + - JSON.stringify(tripStartTransition.data) + - ' end = ' + - JSON.stringify(tripEndTransition.data.ts), + 'tripStartPoint = ' + + JSON.stringify(tripStartPoint) + + 'tripEndPoint = ' + + JSON.stringify(tripEndPoint), ); - } + // if we get a list but our start and end are undefined + // let's print out the complete original list to get a clue + // this should help with debugging + // https://github.com/e-mission/e-mission-docs/issues/417 + // if it ever occurs again + if (angular.isUndefined(tripStartPoint) || angular.isUndefined(tripEndPoint)) { + Logger.log('BUG 417 check: locationList = ' + JSON.stringify(locationList)); + Logger.log( + 'transitions: start = ' + + JSON.stringify(tripStartTransition.data) + + ' end = ' + + JSON.stringify(tripEndTransition.data.ts), + ); + } - const tripProps = points2TripProps(filteredLocationList); - - return { - ...tripProps, - start_loc: { - type: 'Point', - coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude], - }, - end_loc: { - type: 'Point', - coordinates: [tripEndPoint.data.longitude, tripEndPoint.data.latitude], - }, - }; - }); + const tripProps = points2TripProps(filteredLocationList); + + return { + ...tripProps, + start_loc: { + type: 'Point', + coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude], + }, + end_loc: { + type: 'Point', + coordinates: [tripEndPoint.data.longitude, tripEndPoint.data.latitude], + }, + }; + }, + ); }; var linkTrips = function (trip1, trip2) { @@ -342,7 +335,8 @@ angular ' -> ' + moment.unix(tq.endTs).toString(), ); - return UnifiedDataLoader.getUnifiedMessagesForInterval('statemachine/transition', tq).then( + const getMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; + return getUnifiedDataForInterval('statemachine/transition', tq, getMethod).then( function (transitionList) { if (transitionList.length == 0) { Logger.log('No unprocessed trips. yay!'); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index d6e36c397..174974a9d 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,7 +1,7 @@ import moment from 'moment'; -import { getAngularService } from '../angular-react-helper'; import { displayError, logDebug } from '../plugin/logger'; import { getBaseModeByKey, getBaseModeByValue } from './diaryHelper'; +import { getUnifiedDataForInterval } from '../services/unifiedDataLoader'; import i18next from 'i18next'; import { UserInputEntry } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; @@ -134,13 +134,13 @@ export async function updateLocalUnprocessedInputs(pipelineRange, appConfig) { * @returns Promise an array with 1) results for labels and 2) results for notes */ export async function updateAllUnprocessedInputs(pipelineRange, appConfig) { - const UnifiedDataLoader = getAngularService('UnifiedDataLoader'); const tq = getUnprocessedInputQuery(pipelineRange); + const getMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; const labelsPromises = keysForLabelInputs(appConfig).map((key) => - UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true), + getUnifiedDataForInterval(key, tq, getMethod), ); const notesPromises = keysForNotesInputs(appConfig).map((key) => - UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true), + getUnifiedDataForInterval(key, tq, getMethod), ); await updateUnprocessedInputs(labelsPromises, notesPromises, appConfig); } diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 392a0ef3b..bdc426e62 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -16,8 +16,8 @@ import Carousel from '../components/Carousel'; import DailyActiveMinutesCard from './DailyActiveMinutesCard'; import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; -import { getAggregateData, getMetrics } from '../commHelper'; -import { displayError, logDebug } from '../plugin/logger'; +import { getAggregateData, getMetrics } from '../services/commHelper'; +import { displayError, logDebug, logWarn } from '../plugin/logger'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -72,7 +72,7 @@ const MetricsTab = () => { setAggMetrics(metrics as MetricsData); } } catch (e) { - displayError(e, t('errors.while-loading-metrics')); + logWarn(e + t('errors.while-loading-metrics')); // replace with displayErr } } diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 768fa9101..f149598a4 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -3,14 +3,13 @@ import { View, StyleSheet } from 'react-native'; import { ActivityIndicator, Button, Surface, Text } from 'react-native-paper'; import { registerUserDone, setRegisterUserDone, setSaveQrDone } from './onboardingHelper'; import { AppContext } from '../App'; -import { getAngularService } from '../angular-react-helper'; import { displayError, logDebug } from '../plugin/logger'; import { useTranslation } from 'react-i18next'; import QrCode, { shareQR } from '../components/QrCode'; import { onboardingStyles } from './OnboardingStack'; import { preloadDemoSurveyResponse } from './SurveyPage'; import { storageSet } from '../plugin/storage'; -import { registerUser } from '../commHelper'; +import { registerUser } from '../services/commHelper'; import { resetDataAndRefresh } from '../config/dynamicConfig'; import { markConsented } from '../splash/startprefs'; import i18next from 'i18next'; diff --git a/www/js/services.js b/www/js/services.js index 444ff94b7..6ed060ed9 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -1,7 +1,7 @@ 'use strict'; import angular from 'angular'; -import { getRawEntries } from './commHelper'; +import { getRawEntries } from './services/commHelper'; angular .module('emission.services', ['emission.plugin.logger']) @@ -31,106 +31,6 @@ angular //}*/ }; }) - .service('UnifiedDataLoader', function ($window, Logger) { - var combineWithDedup = function (list1, list2) { - var combinedList = list1.concat(list2); - return combinedList.filter(function (value, i, array) { - var firstIndexOfValue = array.findIndex(function (element, index, array) { - return element.metadata.write_ts == value.metadata.write_ts; - }); - return firstIndexOfValue == i; - }); - }; - - // TODO: generalize to iterable of promises - var combinedPromise = function (localPromise, remotePromise, combiner) { - return new Promise(function (resolve, reject) { - var localResult = []; - var localError = null; - - var remoteResult = []; - var remoteError = null; - - var localPromiseDone = false; - var remotePromiseDone = false; - - var checkAndResolve = function () { - if (localPromiseDone && remotePromiseDone) { - // time to return from this promise - if (localError && remoteError) { - reject([localError, remoteError]); - } else { - Logger.log( - 'About to dedup localResult = ' + - localResult.length + - 'remoteResult = ' + - remoteResult.length, - ); - var dedupedList = combiner(localResult, remoteResult); - Logger.log('Deduped list = ' + dedupedList.length); - resolve(dedupedList); - } - } - }; - - localPromise - .then( - function (currentLocalResult) { - localResult = currentLocalResult; - localPromiseDone = true; - }, - function (error) { - localResult = []; - localError = error; - localPromiseDone = true; - }, - ) - .then(checkAndResolve); - - remotePromise - .then( - function (currentRemoteResult) { - remoteResult = currentRemoteResult; - remotePromiseDone = true; - }, - function (error) { - remoteResult = []; - remoteError = error; - remotePromiseDone = true; - }, - ) - .then(checkAndResolve); - }); - }; - - // TODO: Generalize this to work for both sensor data and messages - // Do we even need to separate the two kinds of data? - // Alternatively, we can maintain another mapping between key -> type - // Probably in www/json... - this.getUnifiedSensorDataForInterval = function (key, tq) { - var localPromise = $window.cordova.plugins.BEMUserCache.getSensorDataForInterval( - key, - tq, - true, - ); - var remotePromise = getRawEntries([key], tq.startTs, tq.endTs).then( - function (serverResponse) { - return serverResponse.phone_data; - }, - ); - return combinedPromise(localPromise, remotePromise, combineWithDedup); - }; - - this.getUnifiedMessagesForInterval = function (key, tq, withMetadata) { - var localPromise = $window.cordova.plugins.BEMUserCache.getMessagesForInterval(key, tq, true); - var remotePromise = getRawEntries([key], tq.startTs, tq.endTs).then( - function (serverResponse) { - return serverResponse.phone_data; - }, - ); - return combinedPromise(localPromise, remotePromise, combineWithDedup); - }; - }) .service('ControlHelper', function ($window, $ionicPopup, Logger) { this.writeFile = function (fileEntry, resultList) { // Create a FileWriter object for our FileEntry (log.txt). @@ -250,89 +150,4 @@ angular this.getSettings = function () { return window.cordova.plugins.BEMConnectionSettings.getSettings(); }; - }) - - .factory('Chats', function () { - // Might use a resource here that returns a JSON array - - // Some fake testing data - var chats = [ - { - id: 0, - name: 'Ben Sparrow', - lastText: 'You on your way?', - face: 'img/ben.png', - }, - { - id: 1, - name: 'Max Lynx', - lastText: "Hey, it's me", - face: 'img/max.png', - }, - { - id: 2, - name: 'Adam Bradleyson', - lastText: 'I should buy a boat', - face: 'img/adam.jpg', - }, - { - id: 3, - name: 'Perry Governor', - lastText: 'Look at my mukluks!', - face: 'img/perry.png', - }, - { - id: 4, - name: 'Mike Harrington', - lastText: 'This is wicked good ice cream.', - face: 'img/mike.png', - }, - { - id: 5, - name: 'Ben Sparrow', - lastText: 'You on your way again?', - face: 'img/ben.png', - }, - { - id: 6, - name: 'Max Lynx', - lastText: "Hey, it's me again", - face: 'img/max.png', - }, - { - id: 7, - name: 'Adam Bradleyson', - lastText: 'I should buy a boat again', - face: 'img/adam.jpg', - }, - { - id: 8, - name: 'Perry Governor', - lastText: 'Look at my mukluks again!', - face: 'img/perry.png', - }, - { - id: 9, - name: 'Mike Harrington', - lastText: 'This is wicked good ice cream again.', - face: 'img/mike.png', - }, - ]; - - return { - all: function () { - return chats; - }, - remove: function (chat) { - chats.splice(chats.indexOf(chat), 1); - }, - get: function (chatId) { - for (var i = 0; i < chats.length; i++) { - if (chats[i].id === parseInt(chatId)) { - return chats[i]; - } - } - return null; - }, - }; }); diff --git a/www/js/commHelper.ts b/www/js/services/commHelper.ts similarity index 99% rename from www/js/commHelper.ts rename to www/js/services/commHelper.ts index 5f144888b..6dc71160a 100644 --- a/www/js/commHelper.ts +++ b/www/js/services/commHelper.ts @@ -1,5 +1,5 @@ import { DateTime } from 'luxon'; -import { logDebug } from './plugin/logger'; +import { logDebug } from '../plugin/logger'; /** * @param url URL endpoint for the request diff --git a/www/js/services/unifiedDataLoader.ts b/www/js/services/unifiedDataLoader.ts new file mode 100644 index 000000000..00f6e3027 --- /dev/null +++ b/www/js/services/unifiedDataLoader.ts @@ -0,0 +1,68 @@ +import { getRawEntries } from './commHelper'; +import { ServerResponse, ServerData, TimeQuery } from '../types/serverData'; + +/** + * removeDup is a helper function for combinedPromises + * @param list An array of values from a BEMUserCache promise + * @returns an array with duplicate values removed + */ +export const removeDup = function (list: Array>) { + return list.filter(function (value, i, array) { + const firstIndexOfValue = array.findIndex(function (element) { + return element.metadata.write_ts == value.metadata.write_ts; + }); + return firstIndexOfValue == i; + }); +}; + +export const combinedPromises = function ( + promiseList: Array>, + filter: (list: Array) => Array, +) { + if (promiseList.length === 0) { + throw new RangeError('combinedPromises needs input array.length >= 1'); + } + return new Promise(function (resolve, reject) { + Promise.allSettled(promiseList).then( + (results) => { + let allRej = true; + const values = []; + const rejections = []; + results.forEach((item) => { + if (item.status === 'fulfilled') { + if (allRej) allRej = false; + if (item.value.length != 0) values.push(item.value); + } else rejections.push(item.reason); + }); + if (allRej) reject(rejections); + else resolve(filter(values.flat(1))); + }, + (err) => { + reject(err); + }, + ); + }); +}; + +/** + * getUnifiedDataForInterval is a generalized method to fetch data by its timestamps + * @param key string corresponding to a data entry + * @param tq an object that contains interval start and end times + * @param localGetMethod a BEMUserCache method that fetches certain data via a promise + * @returns A promise that evaluates to the all values found within the queried data + */ +export const getUnifiedDataForInterval = function ( + key: string, + tq: TimeQuery, + localGetMethod: (key: string, tq: TimeQuery, flag: boolean) => Promise, +) { + const test = true; + const getPromise = localGetMethod(key, tq, test); + const remotePromise = getRawEntries([key], tq.startTs, tq.endTs).then(function ( + serverResponse: ServerResponse, + ) { + return serverResponse.phone_data; + }); + const promiseList = [getPromise, remotePromise]; + return combinedPromises(promiseList, removeDup); +}; diff --git a/www/js/splash/notifScheduler.js b/www/js/splash/notifScheduler.js index 9ceb0a23e..d44059848 100644 --- a/www/js/splash/notifScheduler.js +++ b/www/js/splash/notifScheduler.js @@ -3,7 +3,7 @@ import angular from 'angular'; import { getConfig } from '../config/dynamicConfig'; import { addStatReading, statKeys } from '../plugin/clientStats'; -import { getUser, updateUser } from '../commHelper'; +import { getUser, updateUser } from '../services/commHelper'; angular .module('emission.splash.notifscheduler', ['emission.services', 'emission.plugin.logger']) diff --git a/www/js/splash/pushNotifySettings.ts b/www/js/splash/pushNotifySettings.ts index f3eb2c029..42fe05c8f 100644 --- a/www/js/splash/pushNotifySettings.ts +++ b/www/js/splash/pushNotifySettings.ts @@ -13,7 +13,7 @@ * notification handling gets more complex, we should consider decoupling it as well. */ -import { updateUser } from '../commHelper'; +import { updateUser } from '../services/commHelper'; import { logDebug, displayError } from '../plugin/logger'; import { publish, subscribe, EVENTS } from '../customEventHandler'; import { isConsented, readConsentState } from './startprefs'; diff --git a/www/js/splash/storeDeviceSettings.ts b/www/js/splash/storeDeviceSettings.ts index 79a34f930..2a4a646b9 100644 --- a/www/js/splash/storeDeviceSettings.ts +++ b/www/js/splash/storeDeviceSettings.ts @@ -1,4 +1,4 @@ -import { updateUser } from '../commHelper'; +import { updateUser } from '../services/commHelper'; import { isConsented, readConsentState } from './startprefs'; import i18next from 'i18next'; import { displayError, logDebug } from '../plugin/logger'; diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index de1f505f3..a7aa9b26c 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -5,7 +5,7 @@ import { ModalProps } from 'react-native-paper'; import useAppConfig from '../../useAppConfig'; import { useTranslation } from 'react-i18next'; import { SurveyOptions, getInstanceStr, saveResponse } from './enketoHelper'; -import { fetchUrlCached } from '../../commHelper'; +import { fetchUrlCached } from '../../services/commHelper'; import { displayError, displayErrorMsg } from '../../plugin/logger'; // import { transform } from 'enketo-transformer/web'; diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index b1e228540..424e364d2 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -3,6 +3,7 @@ import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; import { logDebug } from '../../plugin/logger'; +import { getUnifiedDataForInterval } from '../../services/unifiedDataLoader'; export type PrefillFields = { [key: string]: string }; @@ -105,10 +106,10 @@ const _getMostRecent = (answers) => { * with incremental updates, we may want to revisit this. */ export function loadPreviousResponseForSurvey(dataKey: string) { - const UnifiedDataLoader = getAngularService('UnifiedDataLoader'); const tq = window['cordova'].plugins.BEMUserCache.getAllTimeQuery(); logDebug('loadPreviousResponseForSurvey: dataKey = ' + dataKey + '; tq = ' + tq); - return UnifiedDataLoader.getUnifiedMessagesForInterval(dataKey, tq).then((answers) => + const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; + return getUnifiedDataForInterval(dataKey, tq, getMethod).then((answers) => _getMostRecent(answers), ); } diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 632023313..51674b0c3 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -1,7 +1,7 @@ // may refactor this into a React hook once it's no longer used by any Angular screens import { getAngularService } from '../../angular-react-helper'; -import { fetchUrlCached } from '../../commHelper'; +import { fetchUrlCached } from '../../services/commHelper'; import i18next from 'i18next'; import { logDebug } from '../../plugin/logger'; diff --git a/www/js/types/fileShareTypes.ts b/www/js/types/fileShareTypes.ts new file mode 100644 index 000000000..03b41a161 --- /dev/null +++ b/www/js/types/fileShareTypes.ts @@ -0,0 +1,22 @@ +import { ServerData } from './serverData'; + +export type TimeStampData = ServerData; + +export type RawTimelineData = { + name: string; + ts: number; + reading: number; +}; + +export interface FsWindow extends Window { + requestFileSystem: ( + type: number, + size: number, + successCallback: (fs: any) => void, + errorCallback?: (error: any) => void, + ) => void; + LocalFileSystem: { + TEMPORARY: number; + PERSISTENT: number; + }; +} diff --git a/www/js/types/serverData.ts b/www/js/types/serverData.ts new file mode 100644 index 000000000..46fa9214b --- /dev/null +++ b/www/js/types/serverData.ts @@ -0,0 +1,45 @@ +export type ServerResponse = { + phone_data: Array>; +}; + +export type ServerData = { + data: Type; + metadata: MetaData; + key?: string; + user_id?: { $uuid: string }; + _id?: { $oid: string }; +}; + +export type MetaData = { + key: string; + platform: string; + write_ts: number; + time_zone: string; + write_fmt_time: string; + write_local_dt: LocalDt; +}; + +export type LocalDt = { + minute: number; + hour: number; + second: number; + day: number; + weekday: number; + month: number; + year: number; + timezone: string; +}; + +/* + * The server also supports queries via TimeQueryComponents, which can be split into multiple + * dates. The TimeQuery type was designed for UserCache calls, which only query via the + * `write_ts` time. For more details, please see the following files in /e-mission-server/: + * - /emission/storage/timeseries/tcquery.py : additional timeQueryComponent + * - /emission/storage/timeseries/timeQuery.py : timeQuery object used for `write_ts` queries + * - /emission/net/api/cfc_webapp.py : implementation of `/datastreams/find_enteries/` + */ +export type TimeQuery = { + key: string; + startTs: number; + endTs: number; +};