diff --git a/www/i18n/en.json b/www/i18n/en.json index 59a544dbd..d53b633c2 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -54,7 +54,7 @@ "profile": "Profile", "tracking": "Tracking", "medium-accuracy": "Medium accuracy", - "dark-theme": "Dark theme", + "demographic-survey": "Demographic Survey", "force-sync": "Force sync", "share": "Share", "check-ui-updates": "Check for UI updates", diff --git a/www/js/control/general-settings.js b/www/js/control/general-settings.js index 8beb89ceb..c8abeca71 100644 --- a/www/js/control/general-settings.js +++ b/www/js/control/general-settings.js @@ -21,7 +21,7 @@ angular.module('emission.main.control',['emission.services', ControlCollectionHelper, ControlSyncHelper, ControlTransitionNotifyHelper, CarbonDatasetHelper, - UpdateCheck, + SurveyLaunch, UpdateCheck, CalorieCal, ClientStats, CommHelper, Logger, $translate) { @@ -484,6 +484,13 @@ angular.module('emission.main.control',['emission.services', } }); }; + + $scope.launchDemographicSurvey = function() { + SurveyLaunch.startSurveyPrefilled( + 'https://ee.kobotoolbox.org/x/hEkHk50v', + 'd[/arcEm5iPB4F9CQZR258k4r/group_hg4zz25/Here_is_your_UUID_wh_ake_any_change_to_it]'); + } + $scope.expandDeveloperZone = function() { if ($scope.collectionExpanded()) { $scope.expanded = false; diff --git a/www/js/diary/infinite_scroll_list.js b/www/js/diary/infinite_scroll_list.js index 9a1c67290..30a5c6637 100644 --- a/www/js/diary/infinite_scroll_list.js +++ b/www/js/diary/infinite_scroll_list.js @@ -68,10 +68,10 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet', Logger.log("Received batch of size "+ctList.length); ctList.reverse(); ctList.forEach($scope.populateBasicClasses); - ctList.forEach((trip) => { + ctList.forEach((trip, tIndex) => { trip.userInput = {}; ConfirmHelper.INPUTS.forEach(function(item, index) { - $scope.populateManualInputs(trip, item, $scope.data.manualResultMap[item]); + $scope.populateManualInputs(trip, ctList[tIndex+1], item, $scope.data.manualResultMap[item]); }); }); ctList.forEach(function(trip, index) { @@ -252,11 +252,12 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet', /** * Embed 'inputType' to the trip */ - $scope.populateManualInputs = function (tripgj, inputType, inputList) { + $scope.populateManualInputs = function (tripgj, nextTripgj, inputType, inputList) { // Check unprocessed labels first since they are more recent // Massage the input to meet getUserInputForTrip expectations const unprocessedLabelEntry = DiaryHelper.getUserInputForTrip( {data: {properties: tripgj, features: [{}, {}, {}]}}, + {data: {properties: nextTripgj, features: [{}, {}, {}]}}, inputList); var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined; if (!angular.isDefined(userInputLabel)) { diff --git a/www/js/diary/list.js b/www/js/diary/list.js index e7e7da76d..6045054e4 100644 --- a/www/js/diary/list.js +++ b/www/js/diary/list.js @@ -165,8 +165,8 @@ angular.module('emission.main.diary.list',['ui-leaflet', /** * Embed 'inputType' to the trip */ - $scope.populateInputFromTimeline = function (tripgj, inputType, inputList) { - var userInput = DiaryHelper.getUserInputForTrip(tripgj, inputList); + $scope.populateInputFromTimeline = function (tripgj, nextTripgj, inputType, inputList) { + var userInput = DiaryHelper.getUserInputForTrip(tripgj, nextTripgj, inputList); if (angular.isDefined(userInput)) { // userInput is an object with data + metadata // the label is the "value" from the options @@ -237,15 +237,20 @@ angular.module('emission.main.diary.list',['ui-leaflet', DiaryHelper.directiveForTrip); Timeline.setTripWrappers(currDayTripWrappers); - $scope.data.currDayTripWrappers.forEach(function(tripgj, index, array) { + $scope.data.currDayTripWrappers.forEach(function(tripgj, tripIndex, array) { tripgj.userInput = {}; ConfirmHelper.INPUTS.forEach(function(item, index) { - $scope.populateInputFromTimeline(tripgj, item, $scope.data.unifiedConfirmsResults[item]); + $scope.populateInputFromTimeline(tripgj, array[tripIndex+1], item, $scope.data.unifiedConfirmsResults[item]); }); $scope.populateBasicClasses(tripgj); $scope.populateCommonInfo(tripgj); }); - $ionicScrollDelegate.scrollTop(true); + if ($rootScope.displayingIncident) { + $ionicScrollDelegate.scrollBottom(true); + $rootScope.displayingIncident = false; + } else { + $ionicScrollDelegate.scrollTop(true); + } }); }); @@ -610,7 +615,6 @@ angular.module('emission.main.diary.list',['ui-leaflet', } else { Logger.log("currDay is not defined, load not complete"); } - $rootScope.displayingIncident = false; } }); }); diff --git a/www/js/diary/services.js b/www/js/diary/services.js index 5dcdfaf5c..2ad24e8c2 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -405,7 +405,7 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', " " + ui.data.label + " logged at "+ ui.metadata.write_ts; } - dh.getUserInputForTrip = function(tripgj, userInputList) { + dh.getUserInputForTrip = function(tripgj, nextTripgj, userInputList) { if (userInputList.length < 20) { console.log("Input list = "+userInputList.map(printUserInput)); } @@ -431,13 +431,15 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', +" trip = "+fmtTs(tripProp.start_ts, userInput.metadata.time_zone) +" -> "+fmtTs(tripProp.end_ts, userInput.metadata.time_zone) +" checks are ("+(userInput.data.start_ts >= tripProp.start_ts) - +" || "+(-(userInput.data.start_ts - tripProp.start_ts) <= 5 * 60) + +" && "+(userInput.data.start_ts <= tripProp.end_ts) + +" || "+(-(userInput.data.start_ts - tripProp.start_ts) <= 15 * 60) +") && "+(userInput.data.end_ts <= tripProp.end_ts); console.log(logStr); // Logger.log(logStr); } return (userInput.data.start_ts >= tripProp.start_ts - || -(userInput.data.start_ts - tripProp.start_ts) <= 5 * 60) + && userInput.data.start_ts <= tripProp.end_ts + || -(userInput.data.start_ts - tripProp.start_ts) <= 15 * 60) && userInput.data.end_ts <= tripProp.end_ts; } else { // we know that the trip is cleaned so we can use the fmt_time @@ -448,14 +450,37 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', +" -> "+fmtTs(userInput.data.end_ts, userInput.metadata.time_zone) +" trip = "+tripProp.start_fmt_time +" -> "+tripProp.end_fmt_time - +" checks are "+(userInput.data.start_ts >= tripProp.start_ts) - +" && ("+(userInput.data.end_ts <= tripProp.end_ts) - +" || "+((userInput.data.end_ts - tripProp.end_ts) <= 5 * 60)+")"; + +" start checks are "+(userInput.data.start_ts >= tripProp.start_ts) + +" && "+(userInput.data.start_ts <= tripProp.end_ts) + +" end checks are "+(userInput.data.end_ts <= tripProp.end_ts) + +" || "+((userInput.data.end_ts - tripProp.end_ts) <= 15 * 60)+")"; Logger.log(logStr); } - return userInput.data.start_ts >= tripProp.start_ts - && (userInput.data.end_ts <= tripProp.end_ts || - (userInput.data.end_ts - tripProp.end_ts) <= 5 * 60); + // https://github.com/e-mission/e-mission-docs/issues/476#issuecomment-747222181 + const startChecks = userInput.data.start_ts >= tripProp.start_ts && + userInput.data.start_ts <= tripProp.end_ts; + var endChecks = (userInput.data.end_ts <= tripProp.end_ts || + (userInput.data.end_ts - tripProp.end_ts) <= 15 * 60); + if (startChecks && !endChecks) { + if (angular.isDefined(nextTripgj)) { + endChecks = userInput.data.end_ts <= nextTripgj.data.properties.start_ts; + Logger.log("Second level of end checks when the next trip is defined("+userInput.data.end_ts+" <= "+ nextTripgj.data.properties.start_ts+") = "+endChecks); + } else { + // next trip is not defined, last trip + endChecks = (userInput.data.end_local_dt.day == userInput.data.start_local_dt.day) + Logger.log("Second level of end checks for the last trip of the day"); + Logger.log("compare "+userInput.data.end_local_dt.day + " with " + userInput.data.start_local_dt.day + " = " + endChecks); + } + if (endChecks) { + // If we have flipped the values, check to see that there + // is sufficient overlap + const overlapDuration = Math.min(userInput.data.end_ts, tripProp.end_ts) - Math.max(userInput.data.start_ts, tripProp.start_ts) + Logger.log("Flipped endCheck, overlap("+overlapDuration+ + ")/trip("+tripProp.duration+") = "+ (overlapDuration / tripProp.duration)); + endChecks = (overlapDuration/tripProp.duration) > 0.5; + } + } + return startChecks && endChecks; } }); if (potentialCandidates.length === 0) { diff --git a/www/js/intro.js b/www/js/intro.js index 742d9e1c7..4f8100409 100644 --- a/www/js/intro.js +++ b/www/js/intro.js @@ -2,6 +2,7 @@ angular.module('emission.intro', ['emission.splash.startprefs', 'emission.splash.updatecheck', + 'emission.survey.launch', 'ionic-toast']) .config(function($stateProvider) { @@ -20,7 +21,7 @@ angular.module('emission.intro', ['emission.splash.startprefs', }) .controller('IntroCtrl', function($scope, $state, $window, $ionicSlideBoxDelegate, - $ionicPopup, $ionicHistory, ionicToast, $timeout, CommHelper, StartPrefs, UpdateCheck, $translate) { + $ionicPopup, $ionicHistory, ionicToast, $timeout, CommHelper, StartPrefs, SurveyLaunch, UpdateCheck, $translate) { $scope.platform = $window.device.platform; $scope.osver = $window.device.version.split(".")[0]; @@ -135,12 +136,25 @@ angular.module('emission.intro', ['emission.splash.startprefs', }); }; + // Adapted from https://stackoverflow.com/a/63363662/4040267 + // made available under a CC BY-SA 4.0 license + + $scope.generateRandomToken = function(length) { + var randomInts = window.crypto.getRandomValues(new Uint8Array(length * 2)); + var randomChars = Array.from(randomInts).map((b) => String.fromCharCode(b)); + var randomString = randomChars.join(""); + var validRandomString = window.btoa(randomString).replace(/[+/]/g, ""); + return validRandomString.substring(0, length); + } + $scope.disagree = function() { $state.go('root.main.heatmap'); }; $scope.agree = function() { StartPrefs.markConsented().then(function(response) { + $scope.randomToken = $scope.generateRandomToken(8); + window.Logger.log("Signing in with random token "+$scope.randomToken); $ionicHistory.clearHistory(); if ($state.is('root.intro')) { $scope.next(); @@ -166,12 +180,62 @@ angular.module('emission.intro', ['emission.splash.startprefs', }); alertPopup.then(function(res) { - window.Logger.log(window.Logger.LEVEL_INFO, errorMsg + ' ' + res); - }); + window.Logger.log(window.Logger.LEVEL_INFO, errorMsg + ' ' + res); }); } - $scope.login = function() { - window.cordova.plugins.BEMJWTAuth.signIn().then(function(userEmail) { + $scope.startSurvey = function () { + SurveyLaunch.startSurveyPrefilled( + 'https://ee.kobotoolbox.org/x/hEkHk50v', + 'd[/arcEm5iPB4F9CQZR258k4r/group_hg4zz25/Here_is_your_UUID_wh_ake_any_change_to_it]'); + } + + $scope.tokenToClipboard = function() { + navigator.clipboard.writeText($scope.randomToken); + }; + + $scope.loginNew = function() { + $scope.login($scope.randomToken); + }; + + $scope.loginExisting = function() { + $scope.data = {}; + const tokenPopup = $ionicPopup.show({ + template: '', + title: 'Enter the existing token that you have', + scope: $scope, + buttons: [ + { + text: 'OK', + type: 'button-positive', + onTap: function(e) { + if (!$scope.data.existing_token) { + //don't allow the user to close unless he enters a username + + e.preventDefault(); + } else { + return $scope.data.existing_token; + } + } + },{ + text: 'Cancel', + type: 'button-stable', + onTap: function(e) { + return null; + } + } + ] + }); + tokenPopup.then(function(token) { + if (token != null) { + $scope.login(token); + } + }).catch(function(err) { + $scope.alertError(err); + }); + }; + + $scope.login = function(token) { + window.cordova.plugins.BEMJWTAuth.setPromptedAuthToken(token).then(function(userEmail) { // ionicToast.show(message, position, stick, time); // $scope.next(); ionicToast.show(userEmail, 'middle', false, 2500); @@ -184,6 +248,7 @@ angular.module('emission.intro', ['emission.splash.startprefs', client: retVal }); }); + $scope.startSurvey(); $scope.finish(); }, function(errorResult) { $scope.alertError('User registration error', errorResult); diff --git a/www/js/splash/startprefs.js b/www/js/splash/startprefs.js index a58bdbcb3..ad794d232 100644 --- a/www/js/splash/startprefs.js +++ b/www/js/splash/startprefs.js @@ -167,11 +167,9 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger', var temp = ReferralHandler.getReferralNavigation(); if (temp == 'goals') { return {state: 'root.main.goals', params: {}}; - } else if ($rootScope.tripConfirmParams) { + } else if ($rootScope.displayingIncident) { logger.log("Showing tripconfirm from startprefs"); - var startEndParams = $rootScope.tripConfirmParams; - $rootScope.tripConfirmParams = angular.undefined; - return {state: 'root.main.tripconfirm', params: startEndParams}; + return {state: 'root.main.diary'}; } else if (angular.isDefined($rootScope.redirectTo)) { var redirState = $rootScope.redirectTo; $rootScope.redirectTo = undefined; diff --git a/www/js/tripconfirm/post-trip-prompt.js b/www/js/tripconfirm/post-trip-prompt.js index bd456f281..e9b011427 100644 --- a/www/js/tripconfirm/post-trip-prompt.js +++ b/www/js/tripconfirm/post-trip-prompt.js @@ -119,10 +119,15 @@ angular.module('emission.tripconfirm.posttrip.prompt', ['emission.plugin.logger' }; var displayCompletedTrip = function(notification, eventOpts) { + /* $rootScope.tripConfirmParams = notification.data; Logger.log("About to display completed trip from notification "+ JSON.stringify(notification.data)); $state.go("root.main.tripconfirm", notification.data); + */ + Logger.log("About to go to diary, which now displays draft information"); + $rootScope.displayingIncident = true; + $state.go("root.main.diary"); }; var checkCategory = function(notification) { diff --git a/www/templates/control/main-control.html b/www/templates/control/main-control.html index 21fa43ff5..b988a1757 100644 --- a/www/templates/control/main-control.html +++ b/www/templates/control/main-control.html @@ -23,13 +23,8 @@
-
{{'.dark-theme'}}
- +
{{'.demographic-survey'}}
+
{{carbonDatasetString}}
diff --git a/www/templates/intro/login.html b/www/templates/intro/login.html index 409173bc0..23f045e2a 100644 --- a/www/templates/intro/login.html +++ b/www/templates/intro/login.html @@ -1,15 +1,20 @@
-

Login via google

+

Login via anonymous token

+
Token {{randomToken}}
-Currently, we only support logging in via google, since they support techniques -such as two factor authentication for greater security. Participants at UC -Berkeley can choose to login using either their CalNet ID or a personal gmail -account. +This unique randomly generated token is your identifier in the system. +

 

+Nobody other than you knows that you are associated with this token. If you +want to communicate with the research team about the data collected about you, +please be prepared to provide this token.
- -
+ +If you already have a token from a previous install, you can use it instead to retain the same account. Note that there are no incorrect tokens. If you enter a token that does not match an existing one, we will create a new account. +
+ +