diff --git a/www/js/control/AlertBar.jsx b/www/js/control/AlertBar.jsx new file mode 100644 index 000000000..f640317a4 --- /dev/null +++ b/www/js/control/AlertBar.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import { Modal, Snackbar} from 'react-native-paper'; +import { useTranslation } from "react-i18next"; + +const AlertBar = ({visible, setVisible, messageKey}) => { + const { t } = useTranslation(); + const onDismissSnackBar = () => setVisible(false); + + return ( + setVisible(false)}> + { + onDismissSnackBar() + }, + }}> + {t(messageKey)} + + + ); + }; + +export default AlertBar; \ No newline at end of file diff --git a/www/js/control/ControlDataTable.jsx b/www/js/control/ControlDataTable.jsx index 84f6d724e..796b057ec 100644 --- a/www/js/control/ControlDataTable.jsx +++ b/www/js/control/ControlDataTable.jsx @@ -1,12 +1,9 @@ import React from "react"; import { DataTable } from 'react-native-paper'; -import { angularize } from "../angular-react-helper"; -import { array } from "prop-types"; -// Note the camelCase to dash-case conventions when translating to .html files! // val with explicit call toString() to resolve bool values not showing const ControlDataTable = ({ controlData }) => { - // console.log("Printing data trying to tabulate", controlData); + console.log("Printing data trying to tabulate", controlData); return ( //rows require unique keys! @@ -28,11 +25,5 @@ const styles = { borderLeftColor: 'rgba(0,0,0,0.25)', } } -ControlDataTable.propTypes = { - controlData: array - } - -// need call to angularize to let the React and Angular co-mingle - //second argument is "module path" - can access later as ControlDataTable.module -angularize(ControlDataTable, 'ControlDataTable', 'emission.main.control.dataTable'); + export default ControlDataTable; diff --git a/www/js/control/DemographicsSettingRow.jsx b/www/js/control/DemographicsSettingRow.jsx new file mode 100644 index 000000000..ff91d0921 --- /dev/null +++ b/www/js/control/DemographicsSettingRow.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import { getAngularService } from "../angular-react-helper"; +import SettingRow from "./SettingRow"; + +const DemographicsSettingRow = ({ }) => { + + const EnketoDemographicsService = getAngularService('EnketoDemographicsService'); + const EnketoSurveyLaunch = getAngularService('EnketoSurveyLaunch'); + const $rootScope = getAngularService('$rootScope'); + + // copied from /js/survey/enketo/enketo-demographics.js + function openPopover() { + return EnketoDemographicsService.loadPriorDemographicSurvey().then((lastSurvey) => { + return EnketoSurveyLaunch + .launch($rootScope, 'UserProfileSurvey', { + prefilledSurveyResponse: lastSurvey?.data?.xmlResponse, + showBackButton: true, showFormFooterJumpNav: true + }) + .then(result => { + console.log("demographic survey result ", result); + }); + }); + } + + return +}; + +export default DemographicsSettingRow; \ No newline at end of file diff --git a/www/js/control/ExpandMenu.jsx b/www/js/control/ExpandMenu.jsx new file mode 100644 index 000000000..2f8bb8ef1 --- /dev/null +++ b/www/js/control/ExpandMenu.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import { StyleSheet } from 'react-native'; +import { List, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import { styles as rowStyles } from "./SettingRow"; + +const ExpansionSection = (props) => { + const { t } = useTranslation(); //this accesses the translations + const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors + const [expanded, setExpanded] = React.useState(false); + + const handlePress = () => setExpanded(!expanded); + + return ( + + {props.children} + + ); +}; +const styles = StyleSheet.create({ + section: (surfaceColor) => ({ + justifyContent: 'space-between', + backgroundColor: surfaceColor, + margin: 1, + }), +}); + +export default ExpansionSection; \ No newline at end of file diff --git a/www/js/control/PopOpCode.jsx b/www/js/control/PopOpCode.jsx new file mode 100644 index 000000000..23368459d --- /dev/null +++ b/www/js/control/PopOpCode.jsx @@ -0,0 +1,49 @@ +import React from "react"; +import { Modal, StyleSheet } from 'react-native'; +import { Button, Text, IconButton, Dialog, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import QrCode from "../components/QrCode"; + +const PopOpCode = ({visibilityValue, tokenURL, action, setVis}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + + return ( + setVis(false)} + transparent={true}> + setVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t("general-settings.qrcode")} + + + {t("general-settings.qrcode-share-title")} + action()} style={styles.button}/> + + + + + + + ) +} +const styles = StyleSheet.create({ + dialog: (surfaceColor) => ({ + backgroundColor: surfaceColor, + margin: 1, + }), + title: + { + alignItems: 'center', + justifyContent: 'center', + }, + content: { + alignItems: 'center', + justifyContent: 'center', + }, + button: { + margin: 'auto', + } + }); + +export default PopOpCode; \ No newline at end of file diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx new file mode 100644 index 000000000..7249e93e0 --- /dev/null +++ b/www/js/control/ProfileSettings.jsx @@ -0,0 +1,351 @@ +import React, { useState, useEffect } from "react"; +import { Modal, StyleSheet } from "react-native"; +import { Dialog, Button, useTheme } from "react-native-paper"; +import { angularize, getAngularService } from "../angular-react-helper"; +import { useTranslation } from "react-i18next"; +import ExpansionSection from "./ExpandMenu"; +import SettingRow from "./SettingRow"; +import ControlDataTable from "./ControlDataTable"; +import DemographicsSettingRow from "./DemographicsSettingRow"; +import PopOpCode from "./PopOpCode"; +import ReminderTime from "./ReminderTime" +import useAppConfig from "../useAppConfig"; + +let controlUpdateCompleteListenerRegistered = false; + +//any pure functions can go outside +const ProfileSettings = () => { + // anything that mutates must go in --- depend on props or state... + const { t } = useTranslation(); + const { appConfig, loading } = useAppConfig(); + const { colors } = useTheme(); + + // get the scope of the general-settings.js file + const mainControlEl = document.getElementById('main-control').querySelector('ion-view'); + const settingsScope = angular.element(mainControlEl).scope(); + // grab any variables or functions we need from it like this: + const { settings, logOut, viewPrivacyPolicy, + fixAppStatus, forceSync, openDatePicker, + eraseUserData, refreshScreen, endForceSync, checkConsent, + dummyNotification, invalidateCache, showLog, showSensed, + parseState, userDataSaved, userData, ui_config } = settingsScope; + + //angular services needed + const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); + const UploadHelper = getAngularService('UploadHelper'); + const EmailHelper = getAngularService('EmailHelper'); + const ControlCollectionHelper = getAngularService('ControlCollectionHelper'); + const ControlSyncHelper = getAngularService('ControlSyncHelper'); + const CalorieCal = getAngularService('CalorieCal'); + const KVStore = getAngularService('KVStore'); + const NotificationScheduler = getAngularService('NotificationScheduler'); + + if (!controlUpdateCompleteListenerRegistered) { + settingsScope.$on('control.update.complete', function() { + console.debug("Received control.update.complete event, refreshing screen"); + refreshScreen(); + refreshCollectSettings(); + }); + controlUpdateCompleteListenerRegistered = true; + } + + //functions that come directly from an Angular service + const editCollectionConfig = ControlCollectionHelper.editConfig; + const editSyncConfig = ControlSyncHelper.editConfig; + + //states and variables used to control/create the settings + const [opCodeVis, setOpCodeVis] = useState(false); + const [nukeSetVis, setNukeVis] = useState(false); + const [carbonDataVis, setCarbonDataVis] = useState(false); + const [forceStateVis, setForceStateVis] = useState(false); + const [collectSettings, setCollectSettings] = useState({}); + const [notificationSettings, setNotificationSettings] = useState({}); + + let carbonDatasetString = t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); + const stateActions = [{text: "Initialize", transition: "INITIALIZE"}, + {text: 'Start trip', transition: "EXITED_GEOFENCE"}, + {text: 'End trip', transition: "STOPPED_MOVING"}, + {text: 'Visit ended', transition: "VISIT_ENDED"}, + {text: 'Visit started', transition: "VISIT_STARTED"}, + {text: 'Remote push', transition: "RECEIVED_SILENT_PUSH"}] + + useEffect(() => { + if (appConfig) { + refreshCollectSettings(); + refreshNotificationSettings(); + } + }, [appConfig]); + + async function refreshCollectSettings() { + console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); + const newCollectSettings = {}; + + // refresh collect plugin configuration + const collectionPluginConfig = await ControlCollectionHelper.getCollectionSettings(); + newCollectSettings.config = collectionPluginConfig; + + const collectionPluginState = await ControlCollectionHelper.getState(); + newCollectSettings.state = collectionPluginState; + newCollectSettings.trackingOn = collectionPluginState != "local.state.tracking_stopped" + && collectionPluginState != "STATE_TRACKING_STOPPED"; + + // I am not sure that this is actually needed anymore since https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7 + const geofenceConfig = await KVStore.get("OP_GEOFENCE_CFG"); + newCollectSettings.experimentalGeofenceOn = geofenceConfig != null; + + const isLowAccuracy = ControlCollectionHelper.isMediumAccuracy(); + if (typeof isLowAccuracy != 'undefined') { + newCollectSettings.lowAccuracy = isLowAccuracy; + } + + setCollectSettings(newCollectSettings); + } + + async function refreshNotificationSettings() { + console.debug('about to refreshNotificationSettings, notificationSettings = ', notificationSettings); + const newNotificationSettings ={}; + + if (ui_config?.reminderSchemes) { + const prefs = await NotificationScheduler.getReminderPrefs(); + const m = moment(prefs.reminder_time_of_day, 'HH:mm'); + newNotificationSettings.prefReminderTimeVal = m.toDate(); + const n = moment(newNotificationSettings.prefReminderTimeVal); + newNotificationSettings.prefReminderTime = n.format('LT'); + newNotificationSettings.prefReminderTimeOnLoad = prefs.reminder_time_of_day; + newNotificationSettings.scheduledNotifs = await NotificationScheduler.getScheduledNotifs(); + updatePrefReminderTime(false); + } + + console.log("notification settings before and after", notificationSettings, newNotificationSettings); + setNotificationSettings(newNotificationSettings); + } + + //methods that control the settings + const uploadLog = function () { + UploadHelper.uploadFile("loggerDB") + }; + + const emailLog = function () { + // Passing true, we want to send logs + EmailHelper.sendEmail("loggerDB") + }; + + async function updatePrefReminderTime(storeNewVal=true, newTime){ + console.log(newTime); + if(storeNewVal){ + const m = moment(newTime); + // store in HH:mm + NotificationScheduler.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }).then(() => { + refreshNotificationSettings(); + }); + } + } + + async function userStartStopTracking() { + const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; + ControlCollectionHelper.forceTransition(transitionToForce); + /* the ControlCollectionHelper.forceTransition call above will trigger a + 'control.update.complete' event when it's done, which will trigger refreshCollectSettings. + So we don't need to call refreshCollectSettings here. */ + } + + const toggleLowAccuracy = function() { + ControlCollectionHelper.toggleLowAccuracy(); + refreshCollectSettings(); + } + + const shareQR = function() { + var prepopulateQRMessage = {}; + var qrAddress = "emission://login_token?token="+settings?.auth?.opcode; + prepopulateQRMessage.files = [qrAddress]; + prepopulateQRMessage.url = settings.auth.opcode; + + window.plugins.socialsharing.shareWithOptions(prepopulateQRMessage, function(result) { + console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true + console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) + }, function(msg) { + console.log("Sharing failed with message: " + msg); + }); + } + + const viewQRCode = function(e) { + setOpCodeVis(true); + } + + var prepopulateMessage = { + message: t('general-settings.share-message'), + subject: t('general-settings.share-subject'), + url: t('general-settings.share-url') + } + + const share = function() { + window.plugins.socialsharing.shareWithOptions(prepopulateMessage, function(result) { + console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true + console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) + }, function(msg) { + console.log("Sharing failed with message: " + msg); + }); + } + + //conditional creation of setting sections + let userDataSection; + if(userDataSaved()) + { + userDataSection = + + + ; + } + + let logUploadSection; + console.debug("appConfg: support_upload:", appConfig?.profile_controls?.support_upload); + if (appConfig?.profile_controls?.support_upload) { + logUploadSection = ; + } + + let timePicker; + let notifSchedule; + if (appConfig?.reminderSchemes) + { + timePicker = ; + notifSchedule = <>console.log("")}> + + } + + return ( + <> + + + + + {timePicker} + + + + setCarbonDataVis(true)}> + + + + {logUploadSection} + + + {userDataSection} + + + + + + + {notifSchedule} + + setNukeVis(true)}> + setForceStateVis(true)}> + + + + + + + console.log("")} desc={settings?.clientAppVer}> + + + {/* menu for "nuke data" */} + setNukeVis(false)} + transparent={true}> + setNukeVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('general-settings.clear-data')} + + + + + + + + + + + + {/* menu for "set carbon dataset - only somewhat working" */} + setCarbonDataVis(false)} + transparent={true}> + setCarbonDataVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('general-settings.choose-dataset')} + + {carbonOptions.map((e) => + + )} + + + + + + + + {/* force state sheet */} + setForceStateVis(false)} + transparent={true}> + setForceStateVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {"Force State"} + + {stateActions.map((e) => + + )} + + + + + + + + {/* opcode viewing popup */} + + + ); +}; +const styles = StyleSheet.create({ + dialog: (surfaceColor) => ({ + backgroundColor: surfaceColor, + margin: 1, + }), + monoDesc: { + fontSize: 12, + fontFamily: "monospace", + } + }); + + angularize(ProfileSettings, 'ProfileSettings', 'emission.main.control.profileSettings'); + export default ProfileSettings; diff --git a/www/js/control/ReminderTime.tsx b/www/js/control/ReminderTime.tsx new file mode 100644 index 000000000..40e8485ee --- /dev/null +++ b/www/js/control/ReminderTime.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import { Modal, StyleSheet } from 'react-native'; +import { List, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import { TimePickerModal } from 'react-native-paper-dates'; +import { styles as rowStyles } from './SettingRow'; + +const TimeSelect = ({ visible, setVisible, defaultTime, updateFunc }) => { + + const onDismiss = React.useCallback(() => { + setVisible(false) + }, [setVisible]) + + const onConfirm = React.useCallback( + ({ hours, minutes }) => { + setVisible(false); + const d = new Date(); + d.setHours(hours, minutes); + updateFunc(true, d); + }, + [setVisible, updateFunc] + ); + + return ( + setVisible(false)} + transparent={true}> + + + ) +} + +const ReminderTime = ({ rowText, timeVar, defaultTime, updateFunc }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const [pickTimeVis, setPickTimeVis] = useState(false); + + let rightComponent = ; + + return ( + <> + setPickTimeVis(true)} + right={() => rightComponent} + /> + + + + + ); +}; +const styles = StyleSheet.create({ + item: (surfaceColor) => ({ + justifyContent: 'space-between', + alignContent: 'center', + backgroundColor: surfaceColor, + margin: 1, + }), + }); + +export default ReminderTime; \ No newline at end of file diff --git a/www/js/control/SettingRow.jsx b/www/js/control/SettingRow.jsx new file mode 100644 index 000000000..3caf36d81 --- /dev/null +++ b/www/js/control/SettingRow.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import { StyleSheet } from 'react-native'; +import { List, Switch, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; + +const SettingRow = ({textKey, iconName, action, desc, switchValue, descStyle=undefined}) => { + const { t } = useTranslation(); //this accesses the translations + const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors + + let rightComponent; + if (iconName) { + rightComponent = ; + } else { + rightComponent = ; + } + let descriptionText; + if(desc) { + descriptionText = {desc}; + } else { + descriptionText = ""; + } + + return ( + action(e)} + right={() => rightComponent} + /> + ); +}; +export const styles = StyleSheet.create({ + item: (surfaceColor) => ({ + justifyContent: 'space-between', + alignContent: 'center', + backgroundColor: surfaceColor, + margin: 1, + }), + title: { + fontSize: 14, + marginVertical: 2, + }, + description: { + fontSize: 12, + }, + }); + +export default SettingRow; diff --git a/www/js/control/general-settings.js b/www/js/control/general-settings.js index 0a5fea1ec..023aca98d 100644 --- a/www/js/control/general-settings.js +++ b/www/js/control/general-settings.js @@ -1,8 +1,7 @@ 'use strict'; import angular from 'angular'; -import ControlDataTable from './ControlDataTable'; -import QrCode from '../components/QrCode'; +import ProfileSettings from './ProfileSettings'; angular.module('emission.main.control',['emission.services', 'emission.i18n.utils', @@ -20,8 +19,7 @@ angular.module('emission.main.control',['emission.services', 'emission.survey.enketo.demographics', 'emission.plugin.logger', 'emission.config.dynamic', - QrCode.module, - ControlDataTable.module]) + ProfileSettings.module]) .controller('ControlCtrl', function($scope, $window, $ionicScrollDelegate, $ionicPlatform, @@ -75,17 +73,7 @@ angular.module('emission.main.control',['emission.services', ionicDatePicker.openDatePicker(datepickerObject); }; - $scope.carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - - $scope.uploadLog = function () { - UploadHelper.uploadFile("loggerDB") - }; - - $scope.emailLog = function () { - // Passing true, we want to send logs - EmailHelper.sendEmail("loggerDB") - }; - + //this function used in ProfileSettings to viewPrivacyPolicy $scope.viewPrivacyPolicy = function($event) { // button -> list element -> scroll // const targetEl = $event.currentTarget.parentElement.parentElement; @@ -102,18 +90,7 @@ angular.module('emission.main.control',['emission.services', } } - $scope.viewQRCode = function($event) { - $scope.tokenURL = "emission://login_token?token="+$scope.settings.auth.opcode; - if ($scope.qrp) { - $scope.qrp.show($event); - } else { - $ionicPopover.fromTemplateUrl("templates/control/qrc.html", {scope: $scope}).then((q) => { - $scope.qrp = q; - $scope.qrp.show($event); - }).catch((err) => Logger.displayError("Error while displaying QR Code", err)); - } - } - + //this function used in ProfileSettings to send DummyNotification $scope.dummyNotification = () => { cordova.plugins.notification.local.addActions('dummy-actions', [ { id: 'action', title: 'Yes' }, @@ -128,14 +105,7 @@ angular.module('emission.main.control',['emission.services', }); } - $scope.updatePrefReminderTime = (storeNewVal=true) => { - const m = moment($scope.settings.notification.prefReminderTimeVal); - $scope.settings.notification.prefReminderTime = m.format('LT'); // display in user's locale - if (storeNewVal) - NotificationScheduler.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }); // store in HH:mm - $scope.settings.notification.scheduledNotifs = NotificationScheduler.scheduledNotifs; - } - + //called in ProfileSettings on the AppStatus row $scope.fixAppStatus = function() { $scope.$broadcast("recomputeAppStatus"); $scope.appStatusModal.show(); @@ -163,7 +133,7 @@ angular.module('emission.main.control',['emission.services', gender: userDataFromStorage.gender == 1? i18next.t('gender-male') : i18next.t('gender-female') } for (var i in temp) { - $scope.userData.push({key: i, value: temp[i]}); + $scope.userData.push({key: i, val: temp[i]}); //needs to be val for the data table! } } }); @@ -205,20 +175,6 @@ angular.module('emission.main.control',['emission.services', $scope.refreshScreen(); }); }); - $scope.getLowAccuracy = function() { - // return true: toggle on; return false: toggle off. - var isMediumAccuracy = ControlCollectionHelper.isMediumAccuracy(); - if (!angular.isDefined(isMediumAccuracy)) { - // config not loaded when loading ui, set default as false - // TODO: Read the value if it is not defined. - // Otherwise, don't we have a race with reading? - // we don't really $apply on this field... - return false; - } else { - return isMediumAccuracy; - } - } - $scope.toggleLowAccuracy = ControlCollectionHelper.toggleLowAccuracy; $scope.getConnectURL = function() { ControlHelper.getSettings().then(function(response) { @@ -231,14 +187,6 @@ angular.module('emission.main.control',['emission.services', }); }; - $scope.getCollectionSettings = function() { - ControlCollectionHelper.getCollectionSettings().then(function(showConfig) { - $scope.$apply(function() { - $scope.settings.collect.show_config = showConfig; - }) - }); - }; - $scope.getSyncSettings = function() { ControlSyncHelper.getSyncSettings().then(function(showConfig) { $scope.$apply(function() { @@ -261,17 +209,20 @@ angular.module('emission.main.control',['emission.services', Logger.displayError("while getting opcode, ",error); }); }; + //in ProfileSettings in DevZone $scope.showLog = function() { $state.go("root.main.log"); } + //inProfileSettings in DevZone $scope.showSensed = function() { $state.go("root.main.sensed"); } $scope.getState = function() { return ControlCollectionHelper.getState().then(function(response) { - $scope.$apply(function() { - $scope.settings.collect.state = response; - }); + /* collect state is now stored in ProfileSettings' collectSettings */ + // $scope.$apply(function() { + // $scope.settings.collect.state = response; + // }); return response; }, function(error) { Logger.displayError("while getting current state", error); @@ -294,25 +245,7 @@ angular.module('emission.main.control',['emission.services', }); } - $scope.nukeUserCache = function() { - var nukeChoiceActions = [{text: i18next.t('general-settings.nuke-ui-state-only'), - action: KVStore.clearOnlyLocal}, - {text: i18next.t('general-settings.nuke-native-cache-only'), - action: KVStore.clearOnlyNative}, - {text: i18next.t('general-settings.nuke-everything'), - action: KVStore.clearAll}]; - - $ionicActionSheet.show({ - titleText: i18next.t('general-settings.clear-data'), - cancelText: i18next.t('general-settings.cancel'), - buttons: nukeChoiceActions, - buttonClicked: function(index, button) { - button.action(); - return true; - } - }); - } - + //in ProfileSettings in DevZone $scope.invalidateCache = function() { window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { $scope.$apply(function() { @@ -347,57 +280,28 @@ angular.module('emission.main.control',['emission.services', $scope.refreshScreen(); }); + //in ProfileSettings in DevZone $scope.refreshScreen = function() { console.log("Refreshing screen"); $scope.settings = {}; - $scope.settings.collect = {}; $scope.settings.sync = {}; - $scope.settings.notification = {}; $scope.settings.auth = {}; $scope.settings.connect = {}; $scope.settings.clientAppVer = ClientStats.getAppVersion(); $scope.getConnectURL(); - $scope.getCollectionSettings(); $scope.getSyncSettings(); $scope.getOPCode(); - $scope.getState().then($scope.isTrackingOn).then(function(isTracking) { - $scope.$apply(function() { - console.log("Setting settings.collect.trackingOn = "+isTracking); - $scope.settings.collect.trackingOn = isTracking; - }); - }); - KVStore.get("OP_GEOFENCE_CFG").then(function(storedCfg) { - $scope.$apply(function() { - if (storedCfg == null) { - console.log("Setting settings.collect.experimentalGeofenceOn = false"); - $scope.settings.collect.experimentalGeofenceOn = false; - } else { - console.log("Setting settings.collect.experimentalGeofenceOn = true"); - $scope.settings.collect.experimentalGeofenceOn = true; - } - }); - }); - if ($scope.ui_config.reminderSchemes) { - NotificationScheduler.getReminderPrefs().then((prefs) => { - $scope.$apply(() => { - const m = moment(prefs.reminder_time_of_day, 'HH:mm'); - // defining data used to populate the upcoming display - $scope.settings.notification.scheduledNotifs = NotificationScheduler.scheduledNotifs; - $scope.settings.notification.prefReminderTimeVal = m.toDate(); - $scope.settings.notification.prefReminderTimeOnLoad = prefs.reminder_time_of_day; - $scope.updatePrefReminderTime(false); // update the displayed time - }); - }); - } $scope.getUserData(); }; - $scope.copyToClipboard = (textToCopy) => { - navigator.clipboard.writeText(textToCopy).then(() => { - ionicToast.show('{Copied to clipboard!}', 'bottom', false, 2000); - }); - } + //this feature has been eliminated (as of right now) + // $scope.copyToClipboard = (textToCopy) => { + // navigator.clipboard.writeText(textToCopy).then(() => { + // ionicToast.show('{Copied to clipboard!}', 'bottom', false, 2000); + // }); + // } + //used in ProfileSettings at the profile/logout/opcode row $scope.logOut = function() { $ionicPopup.confirm({ title: i18next.t('general-settings.are-you-sure'), @@ -502,6 +406,7 @@ angular.module('emission.main.control',['emission.services', }) } + //in ProfileSettings in DevZone $scope.endForceSync = function() { /* First, quickly start and end the trip. Let's listen to the promise * result for start so that we ensure ordering */ @@ -515,10 +420,6 @@ angular.module('emission.main.control',['emission.services', }).then($scope.forceSync); } - $scope.forceState = ControlCollectionHelper.forceState; - $scope.editCollectionConfig = ControlCollectionHelper.editConfig; - $scope.editSyncConfig = ControlSyncHelper.editConfig; - $scope.isAndroid = function() { return ionic.Platform.isAndroid(); } @@ -532,34 +433,14 @@ angular.module('emission.main.control',['emission.services', }).then(function(popover) { $scope.syncSettingsPopup = popover; }); - $scope.isTrackingOn = function() { - return $ionicPlatform.ready().then(function() { - if($scope.isAndroid()){ - return $scope.settings.collect.state != "local.state.tracking_stopped"; - } else if ($scope.isIOS()) { - return $scope.settings.collect.state != "STATE_TRACKING_STOPPED"; - } - }); - }; - $scope.userStartStopTracking = function() { - if ($scope.settings.collect.trackingOn){ - return ControlCollectionHelper.forceTransition('STOP_TRACKING'); - } else { - return ControlCollectionHelper.forceTransition('START_TRACKING'); - } - } - $scope.getExpandButtonClass = function() { - return ($scope.expanded)? "icon ion-ios-arrow-up" : "icon ion-ios-arrow-down"; - } - $scope.getUserDataExpandButtonClass = function() { - return ($scope.dataExpanded)? "icon ion-ios-arrow-up" : "icon ion-ios-arrow-down"; - } + //in ProfileSettings in UserData $scope.eraseUserData = function() { CalorieCal.delete().then(function() { $ionicPopup.alert({template: i18next.t('general-settings.user-data-erased')}); }); } + //in ProfileSettings in DevZone -- part of force/edit state $scope.parseState = function(state) { if (state) { if($scope.isAndroid()){ @@ -569,44 +450,20 @@ angular.module('emission.main.control',['emission.services', } } } - $scope.changeCarbonDataset = function() { - $ionicActionSheet.show({ - buttons: CarbonDatasetHelper.getCarbonDatasetOptions(), - titleText: i18next.t('general-settings.choose-dataset'), - cancelText: i18next.t('general-settings.cancel'), - buttonClicked: function(index, button) { - console.log("changeCarbonDataset(): chose locale " + button.value); - CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(button.value); - $scope.carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - return true; - } - }); - }; - $scope.expandDeveloperZone = function() { - if ($scope.collectionExpanded()) { - $scope.expanded = false; - $ionicScrollDelegate.resize(); - $ionicScrollDelegate.scrollTo(0, 0, true); - - } else { - $scope.expanded = true; - $ionicScrollDelegate.resize(); - $ionicScrollDelegate.scrollTo(0, 1000, true); - } - } - $scope.toggleUserData = function() { - if ($scope.dataExpanded) { - $scope.dataExpanded = false; - } else { - $scope.dataExpanded = true; - } - } - $scope.collectionExpanded = function() { - return $scope.expanded; - } - $scope.userDataExpanded = function() { - return $scope.dataExpanded && $scope.userDataSaved(); - } + // //in ProfileSettings change carbon set + // $scope.changeCarbonDataset = function() { + // $ionicActionSheet.show({ + // buttons: CarbonDatasetHelper.getCarbonDatasetOptions(), + // titleText: i18next.t('general-settings.choose-dataset'), + // cancelText: i18next.t('general-settings.cancel'), + // buttonClicked: function(index, button) { + // console.log("changeCarbonDataset(): chose locale " + button.value); + // CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(button.value); + // $scope.carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + // return true; + // } + // }); + // }; var handleNoConsent = function(resultDoc) { $ionicPopup.confirm({template: i18next.t('general-settings.consent-not-found')}) @@ -636,6 +493,7 @@ angular.module('emission.main.control',['emission.services', }); } + //in ProfileSettings in DevZone (above two functions are helpers) $scope.checkConsent = function() { StartPrefs.getConsentDocument().then(function(resultDoc){ if (resultDoc == null) { diff --git a/www/js/splash/notifScheduler.js b/www/js/splash/notifScheduler.js index df89d1bd8..1fccda3e9 100644 --- a/www/js/splash/notifScheduler.js +++ b/www/js/splash/notifScheduler.js @@ -79,6 +79,55 @@ angular.module('emission.splash.notifscheduler', }); } + //new method to fetch notifications + scheduler.getScheduledNotifs = function() { + return new Promise((resolve, reject) => { + /* if the notifications are still in active scheduling it causes problems + anywhere from 0-n of the scheduled notifs are displayed + if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors + */ + if(isScheduling) + { + console.log("requesting fetch while still actively scheduling, waiting on scheduledPromise"); + scheduledPromise.then(() => { + getNotifs().then((notifs) => { + console.log("done scheduling notifs", notifs); + resolve(notifs); + }) + }) + } + else{ + getNotifs().then((notifs) => { + resolve(notifs); + }) + } + }) + } + + //get scheduled notifications from cordova plugin and format them + const getNotifs = function() { + return new Promise((resolve, reject) => { + cordova.plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length){ + console.log("there are no notifications"); + resolve([]); //if none, return empty array + } + + const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing + let scheduledNotifs = []; + scheduledNotifs = notifSubset.map((n) => { + const time = moment(n.trigger.at).format('LT'); + const date = moment(n.trigger.at).format('LL'); + return { + key: date, + val: time + } + }); + resolve(scheduledNotifs); + }); + }) + } + // schedules the notifications using the cordova plugin const scheduleNotifs = (scheme, notifTimes) => { return new Promise((rs) => { @@ -108,7 +157,7 @@ angular.module('emission.splash.notifscheduler', cordova.plugins.notification.local.schedule(nots, () => { debugGetScheduled("After scheduling"); isScheduling = false; - rs(); + rs(); //scheduling promise resolved here }); }); }); @@ -121,20 +170,27 @@ angular.module('emission.splash.notifscheduler', reminder_time_of_day} = await scheduler.getReminderPrefs(); const scheme = _config.reminderSchemes[reminder_assignment]; const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); - cordova.plugins.notification.local.getScheduled((notifs) => { - if (areAlreadyScheduled(notifs, notifTimes)) { - Logger.log("Already scheduled, not scheduling again"); - } else { - // to ensure we don't overlap with the last scheduling() request, - // we'll wait for the previous one to finish before scheduling again - scheduledPromise.then(() => { - if (isScheduling) { - console.log("ERROR: Already scheduling notifications, not scheduling again") - } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); - } - }); - } + + return new Promise((resolve, reject) => { + cordova.plugins.notification.local.getScheduled((notifs) => { + if (areAlreadyScheduled(notifs, notifTimes)) { + Logger.log("Already scheduled, not scheduling again"); + } else { + // to ensure we don't overlap with the last scheduling() request, + // we'll wait for the previous one to finish before scheduling again + scheduledPromise.then(() => { + if (isScheduling) { + console.log("ERROR: Already scheduling notifications, not scheduling again") + } else { + scheduledPromise = scheduleNotifs(scheme, notifTimes); + //enforcing end of scheduling to conisder update through + scheduledPromise.then(() => { + resolve(); + }) + } + }); + } + }); }); } @@ -175,9 +231,14 @@ angular.module('emission.splash.notifscheduler', } scheduler.setReminderPrefs = async (newPrefs) => { - await CommHelper.updateUser(newPrefs); - update(); - + await CommHelper.updateUser(newPrefs) + const updatePromise = new Promise((resolve, reject) => { + //enforcing update before moving on + update().then(() => { + resolve(); + }); + }); + // record the new prefs in client stats scheduler.getReminderPrefs().then((prefs) => { // extract only the relevant fields from the prefs, @@ -191,6 +252,8 @@ angular.module('emission.splash.notifscheduler', reminder_time_of_day }).then(Logger.log("Added reminder prefs to client stats")); }); + + return updatePromise; } $ionicPlatform.ready().then(async () => { diff --git a/www/templates/control/main-control.html b/www/templates/control/main-control.html index eb98d3278..665eaf7e3 100644 --- a/www/templates/control/main-control.html +++ b/www/templates/control/main-control.html @@ -1,170 +1,7 @@ - - -
-
{{settings.auth.opcode}}
-
-
- -
-
{{'control.view-privacy'}}
-
-
-
-
{{'control.view-qrc'}}
-
-
-
-
- {{'control.reminders-time-of-day' | i18next: {time: settings.notification.prefReminderTime} }} -
-
- - -
-
-
-
{{'control.tracking'}}
- -
-
-
{{'control.app-status'}}
-
-
-
-
{{'control.medium-accuracy'}}
- -
-
-
{{carbonDatasetString}}
-
-
-
-
{{'control.force-sync'}}
-
-
-
-
{{'control.share'}}
-
-
+ + + -
-
{{'control.download-json-dump'}}
-
- -
- -
- -
-
{{'control.upload-log'}}
-
-
- -
-
{{'control.email-log'}}
-
-
- -
-
{{'control.user-data'}}
-
-
- -
-
-
{{'control.erase-data'}}
-
-
- - - -
{{entry.key}}
-
{{entry.value}}
-
-
-
- - -
-
{{'control.dev-zone'}}
-
-
- - -
-
-
{{'control.refresh'}}
-
-
-
-
{{'control.end-trip-sync'}}
-
-
-
-
{{'control.check-consent'}}
-
-
-
-
{{'control.dummy-notification'}}
-
-
- -
-
{{'control.upcoming-notifications'}}
-
- - -
-
{{'control.invalidate-cached-docs'}}
-
-
-
-
{{'control.nuke-all'}}
-
-
-
-
{{parseState(settings.collect.state)}}
-
-
-
-
{{'control.check-log'}}
-
-
-
-
{{'control.check-sensed-data'}}
-
-
- - -
-
{{'control.collection'}}
-
-
- - - -
-
{{'control.sync'}}
-
-
- - -
-
{{'control.app-version'}}
-
{{settings.clientAppVer}}
-
diff --git a/www/templates/main.html b/www/templates/main.html index cdc66d406..c3de4adcb 100644 --- a/www/templates/main.html +++ b/www/templates/main.html @@ -18,7 +18,7 @@ - +