diff --git a/.github/workflows/android-automated-sdk-install.yml b/.github/workflows/android-automated-sdk-install.yml index 5e1efbbf5..f4275a98d 100644 --- a/.github/workflows/android-automated-sdk-install.yml +++ b/.github/workflows/android-automated-sdk-install.yml @@ -13,7 +13,7 @@ on: - '.github/workflows/android-automated-sdk-install.yml' schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -27,73 +27,73 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the current SDK root and version - run: | - echo "SDK root before install $ANDROID_SDK_ROOT" - cat $ANDROID_SDK_ROOT/cmdline-tools/latest/source.properties - echo "Existing installed packages" - $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed + # Runs a single command using the runners shell + - name: Print the current SDK root and version + run: | + echo "SDK root before install $ANDROID_SDK_ROOT" + cat $ANDROID_SDK_ROOT/cmdline-tools/latest/source.properties + echo "Existing installed packages" + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed - - name: Install to a new SDK root - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - printf "Y\nY\nY\nY\nY\n" | bash setup/prereq_android_sdk_install.sh + - name: Install to a new SDK root + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + printf "Y\nY\nY\nY\nY\n" | bash setup/prereq_android_sdk_install.sh - - name: Verify that all packages are as expected - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - echo "Comparing $ANDROID_SDK_ROOT and $NEW_ANDROID_SDK_ROOT" - $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/existing_packages - $NEW_ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/new_packages - diff -uw /tmp/existing_packages /tmp/new_packages - echo "Expected differences; emulators, SDK versions, tool versions" + - name: Verify that all packages are as expected + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + echo "Comparing $ANDROID_SDK_ROOT and $NEW_ANDROID_SDK_ROOT" + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/existing_packages + $NEW_ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/new_packages + diff -uw /tmp/existing_packages /tmp/new_packages + echo "Expected differences; emulators, SDK versions, tool versions" - - name: Verify that directory structure is consistent - shell: bash -l -x {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - ls -al $ANDROID_SDK_ROOT - if [ ! -d $ANDROID_SDK_ROOT/emulator ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/build-tools ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/patcher ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/extras ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/platforms ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/platform-tools ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/system-images ]; then exit 1; fi + - name: Verify that directory structure is consistent + shell: bash -l -x {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + ls -al $ANDROID_SDK_ROOT + if [ ! -d $ANDROID_SDK_ROOT/emulator ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/build-tools ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/patcher ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/extras ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/platforms ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/platform-tools ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/system-images ]; then exit 1; fi - - name: Ensure that the path is correct and installed programs are runnable - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - echo "About to run the emulator at $ANDROID_SDK_ROOT/emulator/emulator" - $ANDROID_SDK_ROOT/emulator/emulator -list-avds - echo "About to run the avdmanager at $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" - $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager list avds + - name: Ensure that the path is correct and installed programs are runnable + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + echo "About to run the emulator at $ANDROID_SDK_ROOT/emulator/emulator" + $ANDROID_SDK_ROOT/emulator/emulator -list-avds + echo "About to run the avdmanager at $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager list avds - - name: Setup the cordova environment - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - bash setup/setup_android_native.sh + - name: Setup the cordova environment + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + bash setup/setup_android_native.sh - - name: Ensure that the path is correct and the project can be activated - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - source setup/activate_native.sh - echo "About to run the avdmanager from the path" `which avdmanager` - avdmanager list avd + - name: Ensure that the path is correct and the project can be activated + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + source setup/activate_native.sh + echo "About to run the avdmanager from the path" `which avdmanager` + avdmanager list avd diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 398a82d14..25eb65317 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -7,15 +7,15 @@ name: osx-build-android on: push: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,55 +26,55 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the java and gradle versions - run: | - echo "Default java version" - java -version - echo "Setting to Java 11 instead" - export JAVA_HOME=$JAVA_HOME_11_X64 - java -version - echo "Checking gradle" - which gradle - gradle -version + # Runs a single command using the runners shell + - name: Print the java and gradle versions + run: | + echo "Default java version" + java -version + echo "Setting to Java 11 instead" + export JAVA_HOME=$JAVA_HOME_11_X64 + java -version + echo "Checking gradle" + which gradle + gradle -version - - name: Tries to figure out where android is installed - run: | - echo "Android listed at $ANDROID_SDK_ROOT" - ls -al /opt/ + - name: Tries to figure out where android is installed + run: | + echo "Android listed at $ANDROID_SDK_ROOT" + ls -al /opt/ - - name: Setup the cordova environment - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_11_X64 - bash setup/setup_android_native.sh + - name: Setup the cordova environment + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_11_X64 + bash setup/setup_android_native.sh - - name: Check tool versions - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_11_X64 - source setup/activate_native.sh - echo "cordova version" - npx cordova -version - echo "ionic version" - npx ionic --version - which gradle - echo "gradle version" - gradle -version + - name: Check tool versions + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_11_X64 + source setup/activate_native.sh + echo "cordova version" + npx cordova -version + echo "ionic version" + npx ionic --version + which gradle + echo "gradle version" + gradle -version - - name: Build android - shell: bash -l {0} - run: | - echo $PATH - which gradle - gradle -version - echo "Let's rerun the activation" - source setup/activate_native.sh - export JAVA_HOME=$JAVA_HOME_11_X64 - echo $PATH - which gradle - gradle --version - npx cordova build android + - name: Build android + shell: bash -l {0} + run: | + echo $PATH + which gradle + gradle -version + echo "Let's rerun the activation" + source setup/activate_native.sh + export JAVA_HOME=$JAVA_HOME_11_X64 + echo $PATH + which gradle + gradle --version + npx cordova build android diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 352e56e73..2c4ee344f 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -6,13 +6,13 @@ name: CI # events but only for the master branch on: push: - branches: - - master - - maint_upgrade_** + branches: + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -23,15 +23,15 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Run a one-line script - run: echo Hello, world! + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! - # Runs a set of commands using the runners shell - - name: Run a multi-line script - run: | - echo Add other actions to build, - echo test, and deploy your project. + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml index 8e2ce82bd..ad0ce2f01 100644 --- a/.github/workflows/ios-build.yml +++ b/.github/workflows/ios-build.yml @@ -7,15 +7,15 @@ name: osx-build-ios on: push: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,47 +26,48 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the xcode path - run: xcode-select --print-path + # Runs a single command using the runners shell + - name: Print the xcode path + run: xcode-select --print-path - - name: Print the xcode setup - run: xcodebuild -version -sdk + - name: Print the xcode setup + run: xcodebuild -version -sdk - - name: Print the brew and ruby versions - run: | - echo "brew version is "`brew --version` - echo "ruby version is" `ruby --version` + - name: Print the brew and ruby versions + run: | + echo "brew version is "`brew --version` + echo "ruby version is" `ruby --version` - - name: Print applications through dmg - run: ls /Applications + - name: Print applications through dmg + run: ls /Applications - - name: Print applications through brew - run: brew list --formula + - name: Print applications through brew + run: brew list --formula - - name: Setup the cordova environment - shell: bash -l {0} - run: | - bash setup/setup_ios_native.sh + - name: Setup the cordova environment + shell: bash -l {0} + run: | + bash setup/setup_ios_native.sh - - name: Check tool versions - shell: bash -l {0} - run: | - source setup/activate_native.sh - echo "cordova version" - npx cordova -version - echo "ionic version" - npx ionic --version + - name: Check tool versions + shell: bash -l {0} + run: | + source setup/activate_native.sh + echo "cordova version" + npx cordova -version + echo "ionic version" + npx ionic --version - - name: Build ios - shell: bash -l {0} - run: | - source setup/activate_native.sh - npx cordova build ios + - name: Build ios + shell: bash -l {0} + run: | + source setup/activate_native.sh + npx cordova build ios + + - name: Cleanup the cordova environment + shell: bash -l {0} + run: bash setup/teardown_ios_native.sh - - name: Cleanup the cordova environment - shell: bash -l {0} - run: bash setup/teardown_ios_native.sh diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index 7833abd82..a5e634821 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -7,15 +7,15 @@ name: osx-serve-install on: push: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,40 +26,40 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the xcode path - run: xcode-select --print-path + # Runs a single command using the runners shell + - name: Print the xcode path + run: xcode-select --print-path - - name: Print the xcode setup - run: xcodebuild -version -sdk + - name: Print the xcode setup + run: xcodebuild -version -sdk - - name: Print applications through dmg - run: ls /Applications + - name: Print applications through dmg + run: ls /Applications - - name: Print applications through brew - run: brew list --formula + - name: Print applications through brew + run: brew list --formula - - name: Setup the serve environment - shell: bash -l {0} - run: | - bash setup/setup_serve.sh + - name: Setup the serve environment + shell: bash -l {0} + run: | + bash setup/setup_serve.sh + + - name: Check tool versions + shell: bash -l {0} + run: | + source setup/activate_serve.sh + echo "cordova version" + npx cordova -version + echo "ionic version" + npx ionic --version - - name: Check tool versions - shell: bash -l {0} - run: | - source setup/activate_serve.sh - echo "cordova version" - npx cordova -version - echo "ionic version" - npx ionic --version - - - name: Run Jest tests - shell: bash -l {0} - run: | - npx jest + - name: Run Jest tests + shell: bash -l {0} + run: | + npx jest # TODO: figure out how to check that a server started correctly # - name: Try starting it diff --git a/OpenSourceLicenses.md b/OpenSourceLicenses.md index e727e8524..5b0140821 100644 --- a/OpenSourceLicenses.md +++ b/OpenSourceLicenses.md @@ -1,11 +1,11 @@ This file lists the module dependencies for the project and their licenses. 1. Most of this module code is **not** redistributed, either in source or binary - form. Instead, it is downloaded automatically using package managers and linked - from the code. The module download includes the license and appropriate credit. +form. Instead, it is downloaded automatically using package managers and linked +from the code. The module download includes the license and appropriate credit. 1. So our primary check here is for modules which do not have a license, or - which are GPL licensed. +which are GPL licensed. The original project was created based on the ionic starter tabs template https://github.com/ionic-team/ionic-starter-tabs @@ -17,84 +17,84 @@ These dependencies were checked in over time in order to support libraries that did not have bower entries, or libraries that were modified with minor changes based on bugs. TODO: Go through the modules, determine the changes and submit them as PRs 🚧 -| Module | License | Original code | -| ------------------------------------------ | ----------------------------------- | ----------------------------------------------------- | -| `www/manual_lib/angularjs-nvd3-directives` | Apache | -| `www/manual_lib/fontawesome` | Icons: CC BY 4.0, Code: MIT License | https://fontawesome.com | -| `www/manual_lib/ionic-datepicker` | MIT | https://github.com/rajeshwarpatlolla/ionic-datepicker | -| `www/manual_lib/leaflet` | BSD 2-clause | https://github.com/Leaflet/Leaflet | -| `www/manual_lib/ui-leaflet` | MIT | https://github.com/angular-ui/ui-leaflet 🗄️ | +| Module | License | Original code | +|--------|---------|---------------| +| `www/manual_lib/angularjs-nvd3-directives` | Apache | +| `www/manual_lib/fontawesome` | Icons: CC BY 4.0, Code: MIT License | https://fontawesome.com | +| `www/manual_lib/ionic-datepicker` | MIT | https://github.com/rajeshwarpatlolla/ionic-datepicker | +| `www/manual_lib/leaflet` | BSD 2-clause | https://github.com/Leaflet/Leaflet | +| `www/manual_lib/ui-leaflet` | MIT | https://github.com/angular-ui/ui-leaflet 🗄️ | ## Javascript dependencies installed via bower -| Module | License | -| ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| `www/lib/ionic` | MIT (from [`bower.json`](https://github.com/ionic-team/ionic-bower/blob/v1.3.0/bower.json)) | -| `www/lib/ionic-toast` | MIT | -| `www/lib/moment` | MIT | -| `www/lib/moment-timezone` | MIT | -| `www/lib/Leaflet.awesome-markers` | MIT | -| `www/lib/angular` | MIT | -| `www/lib/angular-animate` | MIT | -| `www/lib/angular-sanitize` | MIT | -| `www/lib/angular-nvd3` | MIT | -| `www/lib/angularLocalStorage` | MIT | -| `www/lib/ng-walkthrough` | MIT | -| `www/lib/animate.css` | MIT | -| `www/lib/nz-tour` | MIT | -| `www/lib/leaflet-plugins` | MIT | -| `www/lib/angularjs-slider` | MIT | -| `www/lib/angular-translate` | MIT | -| `www/lib/angular-translate-loader-static-files` | MIT | -| `www/lib/angular-translate-interpolation-messageformat` | MIT | +| Module | License | +|--------|---------| +| `www/lib/ionic` | MIT (from [`bower.json`](https://github.com/ionic-team/ionic-bower/blob/v1.3.0/bower.json)) | +| `www/lib/ionic-toast` | MIT | +| `www/lib/moment` | MIT | +| `www/lib/moment-timezone` | MIT | +| `www/lib/Leaflet.awesome-markers` | MIT | +| `www/lib/angular` | MIT | +| `www/lib/angular-animate` | MIT | +| `www/lib/angular-sanitize` | MIT | +| `www/lib/angular-nvd3` | MIT | +| `www/lib/angularLocalStorage` | MIT | +| `www/lib/ng-walkthrough` | MIT | +| `www/lib/animate.css` | MIT | +| `www/lib/nz-tour` | MIT | +| `www/lib/leaflet-plugins` | MIT | +| `www/lib/angularjs-slider` | MIT | +| `www/lib/angular-translate` | MIT | +| `www/lib/angular-translate-loader-static-files` | MIT | +| `www/lib/angular-translate-interpolation-messageformat` | MIT | ## Javascript dependencies installed via npm `package.json` Note that some of these are only required for development, not for proper operation. Not sure whether we should list them or not, but it doesn't hurt. -| Module | License | -| --------- | ------- | -| phonegap | Apache | -| fs-extra | MIT | -| klaw-sync | MIT | +| Module | License | +|--------|---------| +| phonegap | Apache | +| fs-extra | MIT | +| klaw-sync | MIT | ## Javascript dependencies installed via npm command line -| Module | License | -| ------- | ------- | -| cordova | Apache | -| bower | MIT | -| ionic | MIT | +| Module | License | +|--------|---------| +| cordova | Apache | +| bower | MIT | +| ionic | MIT | ## Cordova platforms, installed automatically -| Module | License | -| --------------- | ------- | -| cordova-ios | Apache | -| cordova-android | Apache | +| Module | License | +|--------|---------| +| cordova-ios | Apache | +| cordova-android | Apache | ## Cordova plugins, installed automatically -| Module | License | -| ------------------------------------------------------ | ------------ | -| phonegap-plugin-push | MIT | -| ionic-plugin-keyboard | Apache | -| cordova-plugin-app-version | MIT | -| cordova-plugin-file | Apache | -| cordova-plugin-device | Apache | -| cordova-plugin-whitelist | Apache | -| cordova-plugin-customurlscheme | MIT | -| cordova-plugin-email-composer | Apache | -| cordova-plugin-x-socialsharing | MIT | -| cordova-plugin-inappbrowser | Apache | -| de.appplant.cordova.plugin.local-notification-ios9-fix | Apache | -| cordova-plugin-ionic | MIT | -| edu.berkeley.eecs.emission.cordova.auth | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.comm | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.datacollection | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.serversync | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.settings | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.transitionnotify | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.unifiedlogger | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.usercache | BSD 3-clause | +| Module | License | +|--------|---------| +| phonegap-plugin-push | MIT | +| ionic-plugin-keyboard | Apache | +| cordova-plugin-app-version | MIT | +| cordova-plugin-file | Apache | +| cordova-plugin-device | Apache | +| cordova-plugin-whitelist | Apache | +| cordova-plugin-customurlscheme | MIT | +| cordova-plugin-email-composer | Apache | +| cordova-plugin-x-socialsharing | MIT | +| cordova-plugin-inappbrowser | Apache | +| de.appplant.cordova.plugin.local-notification-ios9-fix | Apache | +| cordova-plugin-ionic | MIT | +| edu.berkeley.eecs.emission.cordova.auth | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.comm | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.datacollection | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.serversync | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.settings | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.transitionnotify | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.unifiedlogger | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.usercache | BSD 3-clause | diff --git a/README.md b/README.md index 95551e6e8..a1f23e99a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,22 @@ -## e-mission phone app +e-mission phone app +-------------------- This is the phone component of the e-mission system. :sparkles: This has now been upgraded to cordova android@9.0.0 and iOS@6.0.1 ([details](https://github.com/e-mission/e-mission-docs/issues/554)). It has also been upgraded to [android API 29](https://github.com/e-mission/e-mission-phone/pull/707/), [cordova-lib@10.0.0 and the most recent node and npm versions](https://github.com/e-mission/e-mission-phone/pull/708)It also now supports CI, so we should not have any build issues in the future. The limitations from the [previous upgrade](https://github.com/e-mission/e-mission-docs/issues/519) have all been resolved. This should be ready to build out of the box, after all the configuration files are changed. -## Additional Documentation - +Additional Documentation +--- Additional documentation has been moved to its own repository [e-mission-docs](https://github.com/e-mission/e-mission-docs). Specific e-mission-phone wikis can be found here: https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -## Updating the UI only - +Updating the UI only +--- [![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) -If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). +If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). ### Installing (one-time) @@ -39,100 +40,95 @@ $ cp ..... www/json/connectionConfig.json ``` $ source setup/activate_serve.sh ``` - + ### Running 1. Start the phonegap deployment server and note the URL(s) that the server is listening to. - ``` - $ npm run serve - .... - [phonegap] listening on 10.0.0.14:3000 - [phonegap] listening on 192.168.162.1:3000 - [phonegap] - [phonegap] ctrl-c to stop the server - [phonegap] - .... - ``` - + ``` + $ npm run serve + .... + [phonegap] listening on 10.0.0.14:3000 + [phonegap] listening on 192.168.162.1:3000 + [phonegap] + [phonegap] ctrl-c to stop the server + [phonegap] + .... + ``` + 1. Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" 1. The app will now display the version of e-mission app that is in your local directory -1. The console logs will be displayed back in the server window (prefaced by `[console]`) -1. Breakpoints can be added by connecting through the browser - - + 1. The console logs will be displayed back in the server window (prefaced by `[console]`) + 1. Breakpoints can be added by connecting through the browser - Safari ([enable develop menu](https://support.apple.com/guide/safari/use-the-safari-develop-menu-sfri20948/mac)): Develop -> Simulator -> index.html - Chrome: chrome://inspect -> Remote target (emulator) - + **Ta-da!** :gift: If you change any of the files in the `www` directory, the app will automatically be re-loaded without manually restarting either the server or the app :tada: **Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. -## End to end testing - +End to end testing +--- A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: 1. installing a local server, -2. running it, +2. running it, 3. loading it with test data, and 4. running analysis on it are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). +In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. -## Updating the e-mission-\* plugins or adding new plugins - +Updating the e-mission-\* plugins or adding new plugins +--- [![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) [![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) -## Pre-requisites - +Pre-requisites +--- - the version of xcode used by the CI - - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) - - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) - - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). + - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) + - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) + - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - git - Java 17. Tested with [OpenJDK 17 (Temurin) using Adoptium](https://adoptium.net). - android SDK; install manually or use setup script below. Note that you only need to run this once **per computer**. - - ``` - $ bash setup/prereq_android_sdk_install.sh - ``` + ``` + $ bash setup/prereq_android_sdk_install.sh + ```
Expected output - ``` - Downloading the command line tools for mac - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 100 114M 100 114M 0 0 8092k 0 0:00:14 0:00:14 --:--:-- 8491k - Found downloaded file at /tmp/commandlinetools-mac-8092744_latest.zip - Installing the command line tools - Archive: /tmp/commandlinetools-mac-8092744_latest.zip - ... - Downloading the android SDK. This will take a LONG time and will require you to agree to lots of licenses. - Do you wish to continue? (Y/N)Y - ... - Accept? (y/N): Y - ... - [====== ] 17% Downloading x86_64-23_r33.zip... s - ``` + ``` + Downloading the command line tools for mac + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 100 114M 100 114M 0 0 8092k 0 0:00:14 0:00:14 --:--:-- 8491k + Found downloaded file at /tmp/commandlinetools-mac-8092744_latest.zip + Installing the command line tools + Archive: /tmp/commandlinetools-mac-8092744_latest.zip + ... + Downloading the android SDK. This will take a LONG time and will require you to agree to lots of licenses. + Do you wish to continue? (Y/N)Y + ... + Accept? (y/N): Y + ... + [====== ] 17% Downloading x86_64-23_r33.zip... s + ```
- - if you are not on the most recent version of OSX, `homebrew` - - this allows us to install the current version of cocoapods without - running into ruby incompatibilities - e.g. - https://github.com/CocoaPods/CocoaPods/issues/11763 - -## Important + - this allows us to install the current version of cocoapods without + running into ruby incompatibilities - e.g. + https://github.com/CocoaPods/CocoaPods/issues/11763 +Important +--- Most of the recent issues encountered have been due to incompatible setup. We have now: - - locked down the dependencies, - created setup and teardown scripts to setup self-contained environments with those dependencies, and @@ -141,8 +137,8 @@ have now: If you have setup failures, please compare the configuration in the passing CI builds with your configuration. That is almost certainly the source of the error. -## Installing (one time only) - +Installing (one time only) +--- Run the setup script for the platform you want to build ``` @@ -184,8 +180,8 @@ AND/OR $ npx cordova emulate android ``` -## Creating logos - +Creating logos +--- If you are building your own version of the app, you must have your own logo to avoid app store conficts. Updating the logo is very simple using the [`ionic cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) @@ -193,20 +189,21 @@ command. **Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work -## Troubleshooting +Troubleshooting +--- - Make sure to use `npx ionic` and `npx cordova`. This is because the setup script installs all the modules locally in a self-contained environment using `npm install` and not `npm install -g` - Check the CI to see whether there is a known issue - Run the commands from the script one by one and see which fails - - compare the failed command with the CI logs + - compare the failed command with the CI logs - Another workaround is to delete the local environment and recreate it - - javascript errors: `rm -rf node_modules && npm install` - - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` - -## Beta-testing debugging + - javascript errors: `rm -rf node_modules && npm install` + - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` +Beta-testing debugging +--- If users run into problems, they have the ability to email logs to the maintainer. These logs are in the form of an sqlite3 database, so they have to be opened using `sqlite3`. Alternatively, you can export it to a csv with @@ -221,7 +218,8 @@ $ python bin/csv_export_add_date.py /tmp/loggerDB. $ less /tmp/loggerDB..withdate.log ``` -## Contributing +Contributing +--- Add the main repo as upstream @@ -244,7 +242,6 @@ Generate a pull request from the UI Address my review comments Once I merge the pull request, pull the changes to your fork and delete the branch - ``` $ git checkout master $ git pull upstream master diff --git a/hooks/README.md b/hooks/README.md index 0750672c2..d2563eab1 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -18,7 +18,6 @@ # under the License. # --> - # Cordova Hooks This directory may contain scripts used to customize cordova commands. This @@ -27,10 +26,9 @@ project root. Any scripts you add to these directories will be executed before and after the commands corresponding to the directory name. Useful for integrating your own build systems or integrating with version control systems. -**Remember**: Make your scripts executable. +__Remember__: Make your scripts executable. ## Hook Directories - The following subdirectories will be used for hooks: after_build/ @@ -67,17 +65,19 @@ The following subdirectories will be used for hooks: All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: -- CORDOVA_VERSION - The version of the Cordova-CLI. -- CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). -- CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) -- CORDOVA_HOOK - Path to the hook that is being executed. -- CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) +* CORDOVA_VERSION - The version of the Cordova-CLI. +* CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). +* CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) +* CORDOVA_HOOK - Path to the hook that is being executed. +* CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) If a script returns a non-zero exit code, then the parent cordova command will be aborted. + ## Writing hooks We highly recommend writting your hooks using Node.js so that they are cross-platform. Some good examples are shown here: [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) + diff --git a/hooks/after_platform_add/ios/ios_copy_locales.js b/hooks/after_platform_add/ios/ios_copy_locales.js index e2e86676c..8a1d9eaa3 100755 --- a/hooks/after_platform_add/ios/ios_copy_locales.js +++ b/hooks/after_platform_add/ios/ios_copy_locales.js @@ -4,63 +4,61 @@ var fs = require('fs-extra'); var path = require('path'); var et = require('elementtree'); -const LOG_NAME = 'Copying locales: '; +const LOG_NAME = "Copying locales: "; module.exports = function (context) { - // If ios platform is not installed, don't even execute - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - - if (context.opts.cordova.platforms.indexOf('ios') < 0 || !fs.existsSync(localesFolder)) return; - - console.log(LOG_NAME + 'Retrieving application name...'); - var config_xml = path.join(context.opts.projectRoot, 'config.xml'); - var data = fs.readFileSync(config_xml).toString(); - // If no data then no config.xml - if (data) { - var etree = et.parse(data); - var applicationName = etree.findtext('./name'); - console.log(LOG_NAME + 'Your application is ' + applicationName); + // If ios platform is not installed, don't even execute var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - var languagesFolders = fs.readdirSync(localesFolder); - // It's not problematic but we will remove them to have cleaner logs. - var filterItems = ['.git', 'LICENSE', 'README.md']; - languagesFolders = languagesFolders.filter((item) => !filterItems.includes(item)); - console.log(LOG_NAME + 'Languages found -> ' + languagesFolders); - languagesFolders.forEach(function (language) { - console.log(LOG_NAME + 'I found ' + language + ', I will now copy the files.'); - var platformRes = path.join( - context.opts.projectRoot, - 'platforms/ios/' + applicationName + '/Resources/', - ); - var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); - var languageFolder = localesFolder + '/' + language; + if (context.opts.cordova.platforms.indexOf('ios') < 0 || !fs.existsSync(localesFolder)) + return; + + console.log(LOG_NAME + "Retrieving application name...") + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + var applicationName = etree.findtext('./name'); + console.log(LOG_NAME + "Your application is " + applicationName); + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + + var languagesFolders = fs.readdirSync(localesFolder); + // It's not problematic but we will remove them to have cleaner logs. + var filterItems = ['.git', 'LICENSE', 'README.md'] + languagesFolders = languagesFolders.filter(item => !filterItems.includes(item)); + console.log(LOG_NAME + "Languages found -> " + languagesFolders); + languagesFolders.forEach(function (language) { + console.log(LOG_NAME + 'I found ' + language + ", I will now copy the files.") + var platformRes = path.join(context.opts.projectRoot, 'platforms/ios/' + applicationName + "/Resources/"); + var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); + var languageFolder = localesFolder + "/" + language; - var lproj = '/' + language + '.lproj'; - var lprojFolder = path.join(languageFolder, lproj); - if (fs.existsSync(lprojFolder)) { - console.log(LOG_NAME + 'Copying ' + lprojFolder + ' to ' + platformRes); + var lproj = "/" + language + ".lproj"; + var lprojFolder = path.join(languageFolder, lproj); + if (fs.existsSync(lprojFolder)) { + console.log(LOG_NAME + "Copying " + lprojFolder + " to " + platformRes); - var platformlproj = platformRes + lproj; - if (!fs.existsSync(platformlproj)) { - console.log(LOG_NAME + platformlproj + 'does not exist, I will create it.'); - fs.mkdirSync(platformlproj, { recursive: true }); - } + var platformlproj = platformRes + lproj; + if (!fs.existsSync(platformlproj)) { + console.log(LOG_NAME + platformlproj + "does not exist, I will create it."); + fs.mkdirSync(platformlproj, {recursive: true} ); + } - fs.copySync(lprojFolder, platformlproj); - console.log(LOG_NAME + lprojFolder + 'copied...'); - } else { - console.log(LOG_NAME + lprojFolder + ' not found, I will continue.'); - } + fs.copySync(lprojFolder, platformlproj); + console.log(LOG_NAME + lprojFolder + "copied...") + } else { + console.log(LOG_NAME + lprojFolder + " not found, I will continue.") + } - var languagei18n = path.join(languageFolder, '/i18n/'); - if (fs.existsSync(languagei18n)) { - console.log(LOG_NAME + 'Copying ' + languagei18n + ' to ' + wwwi18n); - fs.copySync(languagei18n, wwwi18n); - console.log(LOG_NAME + languagei18n + 'copied...'); - } else { - console.log(LOG_NAME + languagei18n + ' not found, I will continue.'); - } - }); - } -}; + var languagei18n = path.join(languageFolder, "/i18n/"); + if (fs.existsSync(languagei18n)) { + console.log(LOG_NAME + "Copying " + languagei18n + " to " + wwwi18n); + fs.copySync(languagei18n, wwwi18n); + console.log(LOG_NAME + languagei18n + "copied...") + } else { + console.log(LOG_NAME + languagei18n + " not found, I will continue.") + } + }); + } +} diff --git a/hooks/after_prepare/010_add_platform_class.js b/hooks/after_prepare/010_add_platform_class.js index 19ff5724b..bda3e4158 100755 --- a/hooks/after_prepare/010_add_platform_class.js +++ b/hooks/after_prepare/010_add_platform_class.js @@ -22,19 +22,20 @@ function addPlatformBodyTag(indexPath, platform) { var html = fs.readFileSync(indexPath, 'utf8'); var bodyTag = findBodyTag(html); - if (!bodyTag) return; // no opening body tag, something's wrong + if(!bodyTag) return; // no opening body tag, something's wrong - if (bodyTag.indexOf(platformClass) > -1) return; // already added + if(bodyTag.indexOf(platformClass) > -1) return; // already added var newBodyTag = bodyTag; var classAttr = findClassAttr(bodyTag); - if (classAttr) { + if(classAttr) { // body tag has existing class attribute, add the classname - var endingQuote = classAttr.substring(classAttr.length - 1); - var newClassAttr = classAttr.substring(0, classAttr.length - 1); + var endingQuote = classAttr.substring(classAttr.length-1); + var newClassAttr = classAttr.substring(0, classAttr.length-1); newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; newBodyTag = bodyTag.replace(classAttr, newClassAttr); + } else { // add class attribute to the body tag newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); @@ -45,46 +46,49 @@ function addPlatformBodyTag(indexPath, platform) { fs.writeFileSync(indexPath, html, 'utf8'); process.stdout.write('add to body class: ' + platformClass + '\n'); - } catch (e) { + } catch(e) { process.stdout.write(e); } } function findBodyTag(html) { // get the body tag - try { + try{ return html.match(/])(.*?)>/gi)[0]; - } catch (e) {} + }catch(e){} } function findClassAttr(bodyTag) { // get the body tag's class attribute - try { + try{ return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; - } catch (e) {} + }catch(e){} } if (rootdir) { + // go through each of the platform directories that have been prepared - var platforms = process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []; + var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); - for (var x = 0; x < platforms.length; x++) { + for(var x=0; x "+dstName); + fs.copySync(srcName, dstName); + } + }); }); - }); }; -var copyIconsFromAllDirs = function () { +var copyIconsFromAllDirs = function() { // Ensure that the res directory exists fs.mkdirsSync(androidPlatformsDir); copyAllIcons(androidPlatformsDir); -}; +} var platformList = process.env.CORDOVA_PLATFORMS; if (platformList == undefined) { - console.log('Testing by running standalone script, invoke anyway'); + console.log("Testing by running standalone script, invoke anyway"); copyIconsFromAllDirs(); } else { - var platforms = platformList.split(','); + var platforms = platformList.split(","); if (platforms.indexOf('android') < 0) { - console.log('Android platform not specified, skipping...'); + console.log("Android platform not specified, skipping..."); } else { copyIconsFromAllDirs(); } diff --git a/hooks/after_prepare/020_copy_notification_icons.js b/hooks/after_prepare/020_copy_notification_icons.js index 91ff6a5b0..f50171524 100755 --- a/hooks/after_prepare/020_copy_notification_icons.js +++ b/hooks/after_prepare/020_copy_notification_icons.js @@ -2,46 +2,45 @@ var fs = require('fs-extra'); var path = require('path'); -var klawSync = require('klaw-sync'); +var klawSync = require('klaw-sync') -var androidPlatformsDir = path.resolve(__dirname, '../../platforms/android/res'); +var androidPlatformsDir = path.resolve(__dirname, '../../platforms/android/res'); var notificationIconsList = [ path.resolve(__dirname, '../../resources/android/ic_mood_question'), - path.resolve(__dirname, '../../resources/android/ic_question_answer'), -]; + path.resolve(__dirname, '../../resources/android/ic_question_answer')]; -var copyAllIcons = function (iconDir) { - var densityDirs = klawSync(iconDir, { nofile: true }); - // console.log("densityDirs = "+JSON.stringify(densityDirs)); - densityDirs.forEach(function (dDir) { - var files = klawSync(dDir.path, { nodir: true }); - files.forEach(function (file) { - var dirName = path.basename(dDir.path); - var fileName = path.basename(file.path); - var srcName = path.join(iconDir, dirName, fileName); - var dstName = path.join(androidPlatformsDir, dirName, fileName); - console.log('About to copy file ' + srcName + ' -> ' + dstName); - fs.copySync(srcName, dstName); +var copyAllIcons = function(iconDir) { + var densityDirs = klawSync(iconDir, {nofile: true}) + // console.log("densityDirs = "+JSON.stringify(densityDirs)); + densityDirs.forEach(function(dDir) { + var files = klawSync(dDir.path, {nodir: true}); + files.forEach(function(file) { + var dirName = path.basename(dDir.path); + var fileName = path.basename(file.path); + var srcName = path.join(iconDir, dirName, fileName); + var dstName = path.join(androidPlatformsDir, dirName, fileName); + console.log("About to copy file "+srcName+" -> "+dstName); + fs.copySync(srcName, dstName); + }); }); - }); }; -var copyIconsFromAllDirs = function () { - notificationIconsList.forEach(function (iconDir) { - console.log('About to copy icons from ' + iconDir); +var copyIconsFromAllDirs = function() { + notificationIconsList.forEach(function(iconDir) { + console.log("About to copy icons from "+iconDir); copyAllIcons(iconDir); }); -}; +} var platformList = process.env.CORDOVA_PLATFORMS; if (platformList == undefined) { - console.log('Testing by running standalone script, invoke anyway'); + console.log("Testing by running standalone script, invoke anyway"); copyIconsFromAllDirs(); } else { - var platforms = platformList.split(','); + var platforms = platformList.split(","); if (platforms.indexOf('android') < 0) { - console.log('Android platform not specified, skipping...'); + console.log("Android platform not specified, skipping..."); } else { copyIconsFromAllDirs(); } diff --git a/hooks/before_build/android/android_change_compile_implementation.js b/hooks/before_build/android/android_change_compile_implementation.js index 8a5b51452..0e2e51999 100755 --- a/hooks/before_build/android/android_change_compile_implementation.js +++ b/hooks/before_build/android/android_change_compile_implementation.js @@ -6,46 +6,43 @@ var fs = require('fs'); var path = require('path'); var et = require('elementtree'); -const LOG_NAME = 'Changing compile to implementation '; +const LOG_NAME = "Changing compile to implementation "; var changeCompileToImplementation = function (file) { - if (fs.existsSync(file)) { - fs.readFile(file, 'utf8', function (err, data) { - var result = data.replace('compile', 'implementation'); - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + '' + file + ' updated...'); - }); - }); - } else { - console.error('Could not find file ' + file + ' skipping compile -> implementation change'); - } -}; + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + var result = data.replace("compile", "implementation"); + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); + }); + } else { + console.error("Could not find file "+file+" skipping compile -> implementation change"); + } +} module.exports = function (context) { - // If Android platform is not installed, don't even execute - if (!context.opts.platforms.includes('android')) return; + // If Android platform is not installed, don't even execute + if (!context.opts.platforms.includes('android')) return; - var config_xml = path.join(context.opts.projectRoot, 'config.xml'); - var data = fs.readFileSync(config_xml).toString(); - // If no data then no config.xml - if (data) { - var etree = et.parse(data); - console.log(LOG_NAME + 'Retrieving application name...'); - var applicationName = etree._root.attrib.id; - console.info(LOG_NAME + 'Your application is ' + applicationName); - const splitParts = applicationName.split('.'); - var lastApplicationPart = splitParts[splitParts.length - 1]; + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + console.log(LOG_NAME + "Retrieving application name...") + var applicationName = etree._root.attrib.id; + console.info(LOG_NAME + "Your application is " + applicationName); + const splitParts = applicationName.split(".") + var lastApplicationPart = splitParts[splitParts.length - 1]; - var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/'); + var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/') - console.log(LOG_NAME + 'Updating barcode scanner gradle...'); - var gradleFile = path.join( - platformRoot, - 'phonegap-plugin-barcodescanner/' + lastApplicationPart + '-barcodescanner.gradle', - ); - changeCompileToImplementation(gradleFile); - } else { - throw new Error(LOG_NAME + 'Could not retrieve application name.'); - } -}; + console.log(LOG_NAME + "Updating barcode scanner gradle..."); + var gradleFile = path.join(platformRoot, 'phonegap-plugin-barcodescanner/'+lastApplicationPart+'-barcodescanner.gradle'); + changeCompileToImplementation(gradleFile); + } else { + throw new Error(LOG_NAME + "Could not retrieve application name."); + } +} diff --git a/hooks/before_build/android/android_copy_locales.js b/hooks/before_build/android/android_copy_locales.js index ad5afcb23..27fab1894 100755 --- a/hooks/before_build/android/android_copy_locales.js +++ b/hooks/before_build/android/android_copy_locales.js @@ -2,50 +2,50 @@ var fs = require('fs-extra'); var path = require('path'); -const LOG_NAME = 'Copying locales: '; +const LOG_NAME = "Copying locales: "; module.exports = function (context) { - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - - // If Android platform is not installed, don't even execute - if (context.opts.cordova.platforms.indexOf('android') < 0 || !fs.existsSync(localesFolder)) - return; - - var languagesFolders = fs.readdirSync(localesFolder); - // It's not problematic but we will remove them to have cleaner logs. - var filterItems = ['.git', 'LICENSE', 'README.md']; - languagesFolders = languagesFolders.filter((item) => !filterItems.includes(item)); - console.log(LOG_NAME + 'Languages found -> ' + languagesFolders); - languagesFolders.forEach(function (language) { - console.log(LOG_NAME + 'I found ' + language + ', I will now copy the files.'); - var platformRes = path.join(context.opts.projectRoot, 'platforms/android/app/src/main/res'); - var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); - var languageFolder = localesFolder + '/' + language; - - var values = '/values-' + language; - var valuesFolder = path.join(languageFolder, values); - if (fs.existsSync(valuesFolder)) { - console.log(LOG_NAME + 'Copying ' + valuesFolder + ' to ' + platformRes); - - var platformValues = platformRes + values; - if (!fs.existsSync(platformValues)) { - console.log(LOG_NAME + platformValues + 'does not exist, I will create it.'); - fs.mkdirSync(platformValues, { recursive: true }); - } - - fs.copySync(valuesFolder, platformValues); - console.log(LOG_NAME + valuesFolder + 'copied...'); - } else { - console.log(LOG_NAME + valuesFolder + ' not found, I will continue.'); - } - - var languagei18n = path.join(languageFolder, '/i18n/'); - if (fs.existsSync(languagei18n)) { - console.log(LOG_NAME + 'Copying ' + languagei18n + ' to ' + wwwi18n); - fs.copySync(languagei18n, wwwi18n); - console.log(LOG_NAME + languagei18n + 'copied...'); - } else { - console.log(LOG_NAME + languagei18n + ' not found, I will continue.'); - } - }); -}; + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + + // If Android platform is not installed, don't even execute + if (context.opts.cordova.platforms.indexOf('android') < 0 || !fs.existsSync(localesFolder)) + return; + + var languagesFolders = fs.readdirSync(localesFolder); + // It's not problematic but we will remove them to have cleaner logs. + var filterItems = ['.git', 'LICENSE', 'README.md'] + languagesFolders = languagesFolders.filter(item => !filterItems.includes(item)); + console.log(LOG_NAME + "Languages found -> " + languagesFolders); + languagesFolders.forEach(function (language) { + console.log(LOG_NAME + 'I found ' + language + ", I will now copy the files.") + var platformRes = path.join(context.opts.projectRoot, 'platforms/android/app/src/main/res'); + var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); + var languageFolder = localesFolder + "/" + language; + + var values = "/values-" + language; + var valuesFolder = path.join(languageFolder, values); + if (fs.existsSync(valuesFolder)) { + console.log(LOG_NAME + "Copying " + valuesFolder + " to " + platformRes); + + var platformValues = platformRes + values; + if (!fs.existsSync(platformValues)) { + console.log(LOG_NAME + platformValues + "does not exist, I will create it."); + fs.mkdirSync(platformValues, {recursive: true}); + } + + fs.copySync(valuesFolder, platformValues); + console.log(LOG_NAME + valuesFolder + "copied...") + } else { + console.log(LOG_NAME + valuesFolder + " not found, I will continue.") + } + + var languagei18n = path.join(languageFolder, "/i18n/"); + if (fs.existsSync(languagei18n)) { + console.log(LOG_NAME + "Copying " + languagei18n + " to " + wwwi18n); + fs.copySync(languagei18n, wwwi18n); + console.log(LOG_NAME + languagei18n + "copied...") + } else { + console.log(LOG_NAME + languagei18n + " not found, I will continue.") + } + }); +} diff --git a/hooks/before_build/android/android_set_provider.js b/hooks/before_build/android/android_set_provider.js index 196def525..3200ab64e 100755 --- a/hooks/before_build/android/android_set_provider.js +++ b/hooks/before_build/android/android_set_provider.js @@ -6,105 +6,107 @@ var fs = require('fs'); var path = require('path'); var et = require('elementtree'); -const PROVIDER = 'edu.berkeley.eecs.emission.provider'; -const ACCOUNT_TYPE = 'eecs.berkeley.edu'; -const LOG_NAME = 'Changing Providers: '; +const PROVIDER = "edu.berkeley.eecs.emission.provider"; +const ACCOUNT_TYPE = "eecs.berkeley.edu"; +const LOG_NAME = "Changing Providers: "; var changeProvider = function (file, currentName, newName) { - if (fs.existsSync(file)) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); - } + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + + var regEx = new RegExp(currentName, 'g'); + + var result = data.replace(regEx, newName + '.provider'); + + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); + }); + } +} - var regEx = new RegExp(currentName, 'g'); +var changeAccountType = function (file, currentName, newName) { + if (fs.existsSync(file)) { - var result = data.replace(regEx, newName + '.provider'); + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + '' + file + ' updated...'); - }); - }); - } -}; + var regEx = new RegExp(currentName, 'g'); -var changeAccountType = function (file, currentName, newName) { - if (fs.existsSync(file)) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); - } + var result = data.replace(regEx, newName); - var regEx = new RegExp(currentName, 'g'); + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error('Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); - var result = data.replace(regEx, newName); - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error('Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + '' + file + ' updated...'); - }); - }); - } -}; + }); + } +} + var changeAccountTypeAndProvider = function (file, accountType, providerName, newName) { - if (fs.existsSync(file)) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); - } - - var regEx1 = new RegExp(accountType, 'g'); - var regEx2 = new RegExp(providerName, 'g'); - - var result = data.replace(regEx1, newName); - result = result.replace(regEx2, newName + '.provider'); - - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + '' + file + ' updated...'); - }); - }); - } else { - console.error(LOG_NAME + 'File ' + file + ' does not exist'); - } -}; + if (fs.existsSync(file)) { + + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + + var regEx1 = new RegExp(accountType, 'g'); + var regEx2 = new RegExp(providerName, 'g'); + + var result = data.replace(regEx1, newName); + result = result.replace(regEx2, newName + '.provider'); + + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); + }); + } else { + console.error(LOG_NAME + "File "+file+" does not exist"); + } +} module.exports = function (context) { - // If Android platform is not installed, don't even execute - if (!context.opts.platforms.includes('android')) return; - - console.log(LOG_NAME + 'Retrieving application name...'); - var config_xml = path.join(context.opts.projectRoot, 'config.xml'); - var data = fs.readFileSync(config_xml).toString(); - // If no data then no config.xml - if (data) { - var etree = et.parse(data); - var applicationName = etree._root.attrib.id; - console.log(LOG_NAME + 'Your application is ' + applicationName); - - var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/app/src/main'); - - console.log(LOG_NAME + 'Updating AndroidManifest.xml...'); - var androidManifest = path.join(platformRoot, 'AndroidManifest.xml'); - changeProvider(androidManifest, PROVIDER, applicationName); - - console.log(LOG_NAME + 'Updating syncadapter.xml'); - var syncAdapter = path.join(platformRoot, 'res/xml/syncadapter.xml'); - changeAccountTypeAndProvider(syncAdapter, ACCOUNT_TYPE, PROVIDER, applicationName); - - console.log(LOG_NAME + 'Updating authenticator.xml'); - var authenticator = path.join(platformRoot, 'res/xml/authenticator.xml'); - changeAccountType(authenticator, ACCOUNT_TYPE, applicationName); - - console.log(LOG_NAME + 'Updating ServerSyncPlugin.java'); - var serverSyncPlugin = path.join( - platformRoot, - 'java/edu/berkeley/eecs/emission/cordova/serversync/ServerSyncPlugin.java', - ); - changeAccountTypeAndProvider(serverSyncPlugin, ACCOUNT_TYPE, PROVIDER, applicationName); - } else { - throw new Error(LOG_NAME + 'Could not retrieve application name.'); - } -}; + // If Android platform is not installed, don't even execute + if (!context.opts.platforms.includes('android')) return; + + console.log(LOG_NAME + "Retrieving application name...") + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + var applicationName = etree._root.attrib.id; + console.log(LOG_NAME + "Your application is " + applicationName); + + var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/app/src/main') + + console.log(LOG_NAME + "Updating AndroidManifest.xml..."); + var androidManifest = path.join(platformRoot, 'AndroidManifest.xml'); + changeProvider(androidManifest, PROVIDER, applicationName); + + console.log(LOG_NAME + "Updating syncadapter.xml"); + var syncAdapter = path.join(platformRoot, 'res/xml/syncadapter.xml'); + changeAccountTypeAndProvider(syncAdapter, ACCOUNT_TYPE, PROVIDER, applicationName); + + console.log(LOG_NAME + "Updating authenticator.xml"); + var authenticator = path.join(platformRoot, 'res/xml/authenticator.xml'); + changeAccountType(authenticator, ACCOUNT_TYPE, applicationName); + + console.log(LOG_NAME + "Updating ServerSyncPlugin.java"); + var serverSyncPlugin = path.join(platformRoot, 'java/edu/berkeley/eecs/emission/cordova/serversync/ServerSyncPlugin.java'); + changeAccountTypeAndProvider(serverSyncPlugin, ACCOUNT_TYPE, PROVIDER, applicationName); + } else { + throw new Error(LOG_NAME + "Could not retrieve application name."); + } +} diff --git a/hooks/before_prepare/download_translation.js b/hooks/before_prepare/download_translation.js index 4cefde8cc..d5d6a88f7 100755 --- a/hooks/before_prepare/download_translation.js +++ b/hooks/before_prepare/download_translation.js @@ -1,57 +1,46 @@ #!/usr/bin/env node -'use strict'; +'use strict' -var child_process = require('child_process'); +var child_process = require('child_process') var fs = require('fs-extra'); var path = require('path'); -const LOG_NAME = 'Downloading locales: '; -const CONF_FILE = 'bin/conf/translate_config.json'; +const LOG_NAME = "Downloading locales: "; +const CONF_FILE = "bin/conf/translate_config.json"; module.exports = function (context) { - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - var confFile = path.join(context.opts.projectRoot, CONF_FILE); + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + var confFile = path.join(context.opts.projectRoot, CONF_FILE); - // Checking if git is installed, return error if not. - try { - child_process.execSync('which git', { stdio: 'inherit' }); - } catch (err) { - console.error(LOG_NAME + 'git not found, (' + err + ')'); - return; - } - - var url = ''; - if (fs.existsSync(confFile)) { - console.log(LOG_NAME + confFile + ' found, I will extract translate repo from it.'); - var data = fs.readFileSync(confFile, 'utf8'); - url = JSON.parse(data).url; - } else { - console.log( - LOG_NAME + - confFile + - ' not found, I will extract translate repo from translation_config.json.sample.', - ); - confFile = confFile + '.sample'; + // Checking if git is installed, return error if not. + try { + child_process.execSync('which git', {'stdio': 'inherit' }); + } catch (err) { + console.error(LOG_NAME + 'git not found, (' + err + ')'); + return; + } + + var url = ""; if (fs.existsSync(confFile)) { - var data = fs.readFileSync(confFile, 'utf8'); - url = JSON.parse(data).url; + console.log(LOG_NAME + confFile + " found, I will extract translate repo from it."); + var data = fs.readFileSync(confFile, 'utf8'); + url = JSON.parse(data).url; } else { - console.log( - LOG_NAME + - confFile + - ' not found, you can find a sample at bin/conf in the e-mission-phone repo.', - ); - return; - } - } + console.log(LOG_NAME + confFile + " not found, I will extract translate repo from translation_config.json.sample."); + confFile = confFile + ".sample"; + if (fs.existsSync(confFile)) { + var data = fs.readFileSync(confFile, 'utf8'); + url = JSON.parse(data).url; + } else { + console.log(LOG_NAME + confFile + " not found, you can find a sample at bin/conf in the e-mission-phone repo."); + return; + } + } - if (!fs.existsSync(localesFolder)) { - console.log(LOG_NAME + 'I will clone from ' + url); - child_process.execSync('git clone ' + url + ' ' + localesFolder, { - timeout: 10000, - stdio: 'inherit', - }); - } else { - child_process.execSync('git pull', { cwd: localesFolder, timeout: 10000, stdio: 'inherit' }); - } -}; + if (!fs.existsSync(localesFolder)) { + console.log(LOG_NAME + "I will clone from " + url); + child_process.execSync('git clone ' + url + ' ' + localesFolder, { 'timeout': 10000, 'stdio': 'inherit'}); + } else { + child_process.execSync('git pull', { 'cwd': localesFolder, 'timeout': 10000, 'stdio': 'inherit' }); + } +} diff --git a/hooks/before_prepare/ios_use_apns_token.js b/hooks/before_prepare/ios_use_apns_token.js index 36be89ce8..0c1b808f5 100755 --- a/hooks/before_prepare/ios_use_apns_token.js +++ b/hooks/before_prepare/ios_use_apns_token.js @@ -1,22 +1,22 @@ #!/usr/bin/env node -'use strict'; +'use strict' var fs = require('fs-extra'); -const LOG_NAME = 'Setting iOS push: FCM = false, APNS = true'; -const CONF_FILE = 'GoogleServicesInfo.plist'; +const LOG_NAME = "Setting iOS push: FCM = false, APNS = true"; +const CONF_FILE = "GoogleServicesInfo.plist"; module.exports = function (context) { - const FCM_TOKEN_SETTING = new RegEx('IS_GCM_ENABLED(\n\\s*)', 'g'); - if (!ctx.opts.platforms.includes('ios')) return; - if (fs.existsSync(confFile)) { - console.log(LOG_NAME + confFile + ' found, modifying it'); - var regEx = new RegExp(currentName, 'g'); + const FCM_TOKEN_SETTING = new RegEx("IS_GCM_ENABLED(\n\\s*)", "g"); + if (!ctx.opts.platforms.includes('ios')) return; + if (fs.existsSync(confFile)) { + console.log(LOG_NAME + confFile + " found, modifying it"); + var regEx = new RegExp(currentName, 'g'); - var data = fs.readFileSync(confFile, 'utf8'); - var replacedData = data.replace(regEx, 'IS_GCM_ENABLED$1'); - fs.writeFileSync(CONF_FILE, replacedData, 'utf8'); - console.log(LOG_NAME + confFile + ' modified file written'); - } -}; + var data = fs.readFileSync(confFile, 'utf8'); + var replacedData = data.replace(regEx, "IS_GCM_ENABLED$1"); + fs.writeFileSync(CONF_FILE, replacedData, 'utf8'); + console.log(LOG_NAME + confFile + " modified file written"); + } +} diff --git a/jest.config.json b/jest.config.json index 725f3c51f..78dc839b4 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,5 +1,11 @@ { - "testPathIgnorePatterns": ["/node_modules/", "/platforms/", "/plugins/", "/lib/", "/manual_lib/"], + "testPathIgnorePatterns": [ + "/node_modules/", + "/platforms/", + "/plugins/", + "/lib/", + "/manual_lib/" + ], "transform": { "^.+\\.(ts|tsx|js|jsx)$": "ts-jest" }, diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 0b0763567..943f06520 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -49,7 +49,10 @@ "webpack-cli": "^5.0.1" }, "cordova": { - "platforms": ["android", "ios"], + "platforms": [ + "android", + "ios" + ], "plugins": { "@havesource/cordova-plugin-push": { "ANDROIDX_CORE_VERSION": "1.6.+", diff --git a/package.serve.json b/package.serve.json index 334d21e6d..2bba5509f 100644 --- a/package.serve.json +++ b/package.serve.json @@ -99,6 +99,8 @@ "shelljs": "^0.8.5" }, "cordova": { - "platforms": ["browser"] + "platforms": [ + "browser" + ] } } diff --git a/tsconfig.json b/tsconfig.json index 28ea586ca..29384751e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "moduleResolution": "node" }, "include": ["www/**/*"], - "exclude": ["**/www/manual_lib/*", "**/node_modules/*", "**/dist/*"] + "exclude": ["**/www/manual_lib/*", "**/node_modules/*", "**/dist/*"], } diff --git a/webpack.config.js b/webpack.config.js index 55e631b28..d6e36fb18 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,5 @@ -const path = require('path'); -const webpack = require('webpack'); +const path = require('path') +const webpack = require('webpack') module.exports = { entry: './www/index.js', @@ -12,31 +12,25 @@ module.exports = { // to load CSS and SCSS (enketo-core only supplies SCSS) { test: /\.(scss|css)$/, - include: [ - path.resolve(__dirname, 'www/css'), - path.resolve(__dirname, 'www/manual_lib'), - path.resolve(__dirname, 'node_modules/enketo-core'), - path.resolve(__dirname, 'node_modules/leaflet'), - ], + include: [path.resolve(__dirname, 'www/css'), + path.resolve(__dirname, 'www/manual_lib'), + path.resolve(__dirname, 'node_modules/enketo-core'), + path.resolve(__dirname, 'node_modules/leaflet')], use: ['style-loader', 'css-loader', 'sass-loader'], }, // to resolve url() in CSS { test: /\.(png|jpg)$/, - include: [ - path.resolve(__dirname, 'www/css'), - path.resolve(__dirname, 'node_modules/react-native-paper'), - path.resolve(__dirname, 'node_modules/@react-navigation/elements'), - ], + include: [path.resolve(__dirname, 'www/css'), + path.resolve(__dirname, 'node_modules/react-native-paper'), + path.resolve(__dirname, 'node_modules/@react-navigation/elements')], use: 'url-loader', }, // necessary for react-native-web to bundle JSX { test: /\.(js|jsx|ts|tsx)$/, - include: [ - path.resolve(__dirname, 'www'), - path.resolve(__dirname, 'node_modules/react-native-vector-icons'), - ], + include: [path.resolve(__dirname, 'www'), + path.resolve(__dirname, 'node_modules/react-native-vector-icons')], loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], @@ -55,10 +49,8 @@ module.exports = { // necessary for react-native-paper to load images, fonts, and vector graphics { test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/, - include: [ - path.resolve(__dirname, 'www'), - path.resolve(__dirname, 'node_modules/react-native-vector-icons'), - ], + include: [path.resolve(__dirname, 'www'), + path.resolve(__dirname, 'node_modules/react-native-vector-icons')], type: 'asset/resource', }, ], @@ -86,8 +78,8 @@ module.exports = { /* Enketo expects its per-app configuration to be available as 'enketo-config', so we have to alias it here. https://github.com/enketo/enketo-core#global-configuration */ - 'enketo/config': path.resolve(__dirname, 'www/js/config/enketo-config'), + 'enketo/config': path.resolve(__dirname, 'www/js/config/enketo-config') }, extensions: ['.web.js', '.jsx', '.tsx', '.ts', '.js'], }, -}; +} diff --git a/webpack.prod.js b/webpack.prod.js index 4e61033e4..c08fc140c 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -1,4 +1,4 @@ -const path = require('path'); +const path = require('path') const common = require('./webpack.config.js'); const { merge } = require('webpack-merge'); @@ -21,7 +21,7 @@ module.exports = merge(common, { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], - plugins: ['angularjs-annotate'], + plugins: ["angularjs-annotate"], }, }, { diff --git a/www/__tests__/diaryHelper.test.ts b/www/__tests__/diaryHelper.test.ts index 1ac143334..822b19bba 100644 --- a/www/__tests__/diaryHelper.test.ts +++ b/www/__tests__/diaryHelper.test.ts @@ -1,86 +1,63 @@ -import { - getFormattedDate, - isMultiDay, - getFormattedDateAbbr, - getFormattedTimeRange, - getDetectedModes, - getBaseModeByKey, - modeColors, -} from '../js/diary/diaryHelper'; +import { getFormattedDate, isMultiDay, getFormattedDateAbbr, getFormattedTimeRange, getDetectedModes, getBaseModeByKey, modeColors } from "../js/diary/diaryHelper"; it('returns a formatted date', () => { - expect(getFormattedDate('2023-09-18T00:00:00-07:00')).toBe('Mon September 18, 2023'); - expect(getFormattedDate('')).toBeUndefined(); - expect(getFormattedDate('2023-09-18T00:00:00-07:00', '2023-09-21T00:00:00-07:00')).toBe( - 'Mon September 18, 2023 - Thu September 21, 2023', - ); + expect(getFormattedDate("2023-09-18T00:00:00-07:00")).toBe("Mon September 18, 2023"); + expect(getFormattedDate("")).toBeUndefined(); + expect(getFormattedDate("2023-09-18T00:00:00-07:00", "2023-09-21T00:00:00-07:00")).toBe("Mon September 18, 2023 - Thu September 21, 2023"); }); it('returns an abbreviated formatted date', () => { - expect(getFormattedDateAbbr('2023-09-18T00:00:00-07:00')).toBe('Mon, Sep 18'); - expect(getFormattedDateAbbr('')).toBeUndefined(); - expect(getFormattedDateAbbr('2023-09-18T00:00:00-07:00', '2023-09-21T00:00:00-07:00')).toBe( - 'Mon, Sep 18 - Thu, Sep 21', - ); + expect(getFormattedDateAbbr("2023-09-18T00:00:00-07:00")).toBe("Mon, Sep 18"); + expect(getFormattedDateAbbr("")).toBeUndefined(); + expect(getFormattedDateAbbr("2023-09-18T00:00:00-07:00", "2023-09-21T00:00:00-07:00")).toBe("Mon, Sep 18 - Thu, Sep 21"); }); it('returns a human readable time range', () => { - expect(getFormattedTimeRange('2023-09-18T00:00:00-07:00', '2023-09-18T00:00:00-09:20')).toBe( - '2 hours', - ); - expect(getFormattedTimeRange('2023-09-18T00:00:00-07:00', '2023-09-18T00:00:00-09:30')).toBe( - '3 hours', - ); - expect(getFormattedTimeRange('', '2023-09-18T00:00:00-09:30')).toBeFalsy(); + expect(getFormattedTimeRange("2023-09-18T00:00:00-07:00", "2023-09-18T00:00:00-09:20")).toBe("2 hours"); + expect(getFormattedTimeRange("2023-09-18T00:00:00-07:00", "2023-09-18T00:00:00-09:30")).toBe("3 hours"); + expect(getFormattedTimeRange("", "2023-09-18T00:00:00-09:30")).toBeFalsy(); }); -it('returns a Base Mode for a given key', () => { - expect(getBaseModeByKey('WALKING')).toEqual({ - name: 'WALKING', - icon: 'walk', - color: modeColors.blue, - }); - expect(getBaseModeByKey('MotionTypes.WALKING')).toEqual({ - name: 'WALKING', - icon: 'walk', - color: modeColors.blue, - }); - expect(getBaseModeByKey('I made this type up')).toEqual({ - name: 'UNKNOWN', - icon: 'help', - color: modeColors.grey, - }); +it("returns a Base Mode for a given key", () => { + expect(getBaseModeByKey("WALKING")).toEqual({ name: "WALKING", icon: "walk", color: modeColors.blue }); + expect(getBaseModeByKey("MotionTypes.WALKING")).toEqual({ name: "WALKING", icon: "walk", color: modeColors.blue }); + expect(getBaseModeByKey("I made this type up")).toEqual({ name: "UNKNOWN", icon: "help", color: modeColors.grey }); }); it('returns true/false is multi day', () => { - expect(isMultiDay('2023-09-18T00:00:00-07:00', '2023-09-19T00:00:00-07:00')).toBeTruthy(); - expect(isMultiDay('2023-09-18T00:00:00-07:00', '2023-09-18T00:00:00-09:00')).toBeFalsy(); - expect(isMultiDay('', '2023-09-18T00:00:00-09:00')).toBeFalsy(); + expect(isMultiDay("2023-09-18T00:00:00-07:00", "2023-09-19T00:00:00-07:00")).toBeTruthy(); + expect(isMultiDay("2023-09-18T00:00:00-07:00", "2023-09-18T00:00:00-09:00")).toBeFalsy(); + expect(isMultiDay("", "2023-09-18T00:00:00-09:00")).toBeFalsy(); }); //created a fake trip with relevant sections by examining log statements -let myFakeTrip = { - sections: [ - { sensed_mode_str: 'BICYCLING', distance: 6013.73657416706 }, - { sensed_mode_str: 'WALKING', distance: 715.3078629361006 }, - ], -}; -let myFakeTrip2 = { - sections: [ - { sensed_mode_str: 'BICYCLING', distance: 6013.73657416706 }, - { sensed_mode_str: 'BICYCLING', distance: 715.3078629361006 }, - ], -}; +let myFakeTrip = {sections: [ + { "sensed_mode_str": "BICYCLING", "distance": 6013.73657416706 }, + { "sensed_mode_str": "WALKING", "distance": 715.3078629361006 } +]}; +let myFakeTrip2 = {sections: [ + { "sensed_mode_str": "BICYCLING", "distance": 6013.73657416706 }, + { "sensed_mode_str": "BICYCLING", "distance": 715.3078629361006 } +]}; let myFakeDetectedModes = [ - { mode: 'BICYCLING', icon: 'bike', color: modeColors.green, pct: 89 }, - { mode: 'WALKING', icon: 'walk', color: modeColors.blue, pct: 11 }, -]; + { mode: "BICYCLING", + icon: "bike", + color: modeColors.green, + pct: 89 }, + { mode: "WALKING", + icon: "walk", + color: modeColors.blue, + pct: 11 }]; -let myFakeDetectedModes2 = [{ mode: 'BICYCLING', icon: 'bike', color: modeColors.green, pct: 100 }]; +let myFakeDetectedModes2 = [ + { mode: "BICYCLING", + icon: "bike", + color: modeColors.green, + pct: 100 }]; it('returns the detected modes, with percentages, for a trip', () => { expect(getDetectedModes(myFakeTrip)).toEqual(myFakeDetectedModes); expect(getDetectedModes(myFakeTrip2)).toEqual(myFakeDetectedModes2); expect(getDetectedModes({})).toEqual([]); // empty trip, no sections, no modes -}); +}) diff --git a/www/i18n/en.json b/www/i18n/en.json index 2ee46f8ca..fe0df617a 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -1,429 +1,429 @@ { - "loading": "Loading...", - "pull-to-refresh": "Pull to refresh", + "loading" : "Loading...", + "pull-to-refresh": "Pull to refresh", - "weekdays-all": "All", - "weekdays-select": "Select day of the week", + "weekdays-all": "All", + "weekdays-select": "Select day of the week", - "trip-confirm": { - "services-please-fill-in": "Please fill in the {{text}} not listed.", - "services-cancel": "Cancel", - "services-save": "Save" - }, - - "control": { - "profile": "Profile", - "edit-demographics": "Edit Demographics", - "tracking": "Tracking", - "app-status": "App Status", - "incorrect-app-status": "Please update permissions", - "fix-app-status": "Click to view and fix app status", - "fix": "Fix", - "medium-accuracy": "Medium accuracy", - "force-sync": "Force sync", - "share": "Share", - "download-json-dump": "Download json dump", - "email-log": "Email log", - "upload-log": "Upload log", - "view-privacy": "View Privacy Policy", - "user-data": "User data", - "erase-data": "Erase data", - "dev-zone": "Developer zone", - "refresh": "Refresh", - "end-trip-sync": "End trip + sync", - "check-consent": "Check consent", - "invalidate-cached-docs": "Invalidate cached docs", - "nuke-all": "Nuke all buffers and cache", - "test-notification": "Test local notification", - "check-log": "Check log", - "log-title": "Log", - "check-sensed-data": "Check sensed data", - "sensed-title": "Sensed Data: Transitions", - "collection": "Collection", - "sync": "Sync", - "button-accept": "I accept", - "view-qrc": "My OPcode", - "app-version": "App Version", - "reminders-time-of-day": "Time of Day for Reminders ({{time}})", - "upcoming-notifications": "Upcoming Notifications", - "dummy-notification": "Dummy Notification in 5 Seconds", - "log-out": "Log Out" - }, + "trip-confirm": { + "services-please-fill-in": "Please fill in the {{text}} not listed.", + "services-cancel": "Cancel", + "services-save": "Save" + }, - "general-settings": { - "choose-date": "Choose date to download data", - "choose-dataset": "Choose a dataset for carbon footprint calculations", - "carbon-dataset": "Carbon dataset", - "nuke-ui-state-only": "UI state only", - "nuke-native-cache-only": "Native cache only", - "nuke-everything": "Everything", - "clear-data": "Clear data", - "are-you-sure": "Are you sure?", - "log-out-warning": "You will be logged out and your credentials will not be saved. Unsynced data may be lost.", - "cancel": "Cancel", - "confirm": "Confirm", - "user-data-erased": "User data erased.", - "consent-not-found": "Consent for data collection not found, consent now?", - "no-consent-message": "OK! Note that you won't get any personalized stats until you do!", - "consent-found": "Consent found!", - "consented-to": "Consented to protocol {{protocol_id}}, {{approval_date}}", - "consented-ok": "OK", - "qrcode": "My OPcode", - "qrcode-share-title": "You can save your OPcode to login easily in the future!" - }, + "control":{ + "profile": "Profile", + "edit-demographics": "Edit Demographics", + "tracking": "Tracking", + "app-status": "App Status", + "incorrect-app-status": "Please update permissions", + "fix-app-status": "Click to view and fix app status", + "fix": "Fix", + "medium-accuracy": "Medium accuracy", + "force-sync": "Force sync", + "share": "Share", + "download-json-dump": "Download json dump", + "email-log": "Email log", + "upload-log": "Upload log", + "view-privacy": "View Privacy Policy", + "user-data": "User data", + "erase-data": "Erase data", + "dev-zone": "Developer zone", + "refresh": "Refresh", + "end-trip-sync": "End trip + sync", + "check-consent": "Check consent", + "invalidate-cached-docs": "Invalidate cached docs", + "nuke-all": "Nuke all buffers and cache", + "test-notification": "Test local notification", + "check-log": "Check log", + "log-title" : "Log", + "check-sensed-data": "Check sensed data", + "sensed-title": "Sensed Data: Transitions", + "collection": "Collection", + "sync": "Sync", + "button-accept": "I accept", + "view-qrc": "My OPcode", + "app-version": "App Version", + "reminders-time-of-day": "Time of Day for Reminders ({{time}})", + "upcoming-notifications": "Upcoming Notifications", + "dummy-notification" : "Dummy Notification in 5 Seconds", + "log-out": "Log Out" + }, - "metrics": { - "cancel": "Cancel", - "confirm": "Confirm", - "get": "Get", - "range": "Range", - "filter": "Filter", - "from": "From:", - "to": "To:", - "last-week": "last week", - "frequency": "Frequency:", - "pandafreqoptions-daily": "DAILY", - "pandafreqoptions-weekly": "WEEKLY", - "pandafreqoptions-biweekly": "BIWEEKLY", - "pandafreqoptions-monthly": "MONTHLY", - "pandafreqoptions-yearly": "YEARLY", - "freqoptions-daily": "DAILY", - "freqoptions-monthly": "MONTHLY", - "freqoptions-yearly": "YEARLY", - "select-pandafrequency": "Select summary freqency", - "select-frequency": "Select summary freqency", - "chart-xaxis-date": "Date", - "chart-no-data": "No Data Available", - "trips-yaxis-number": "Number", - "calorie-data-change": " change", - "calorie-data-unknown": "Unknown...", - "greater-than": " greater than ", - "greater": " greater ", - "or": "or", - "less-than": " less than ", - "less": " less ", - "week-before": "vs. week before", - "this-week": "this week", - "pick-a-date": "Pick a date", - "trips": "trips", - "hours": "hours", - "minutes": "minutes", - "custom": "Custom" - }, + "general-settings":{ + "choose-date" : "Choose date to download data", + "choose-dataset" : "Choose a dataset for carbon footprint calculations", + "carbon-dataset" : "Carbon dataset", + "nuke-ui-state-only" : "UI state only", + "nuke-native-cache-only" : "Native cache only", + "nuke-everything" : "Everything", + "clear-data": "Clear data", + "are-you-sure": "Are you sure?", + "log-out-warning": "You will be logged out and your credentials will not be saved. Unsynced data may be lost.", + "cancel": "Cancel", + "confirm": "Confirm", + "user-data-erased": "User data erased.", + "consent-not-found": "Consent for data collection not found, consent now?", + "no-consent-message": "OK! Note that you won't get any personalized stats until you do!", + "consent-found": "Consent found!", + "consented-to": "Consented to protocol {{protocol_id}}, {{approval_date}}", + "consented-ok": "OK", + "qrcode": "My OPcode", + "qrcode-share-title": "You can save your OPcode to login easily in the future!" + }, - "diary": { - "distance-in-time": "{{distance}} {{distsuffix}} in {{time}}", - "distance": "Distance", - "time": "Time", - "mode": "Mode", - "replaces": "Replaces", - "purpose": "Purpose", - "survey": "Details", - "untracked-time-range": "Untracked: {{start}} - {{end}}", - "unlabeled": "All Unlabeled", - "invalid-ebike": "Invalid", - "to-label": "To Label", - "show-all": "All Trips", - "no-trips-found": "No trips found", - "choose-mode": "Mode 📝 ", - "choose-replaced-mode": "Replaces 📝", - "choose-purpose": "Purpose 📝", - "choose-survey": "Add Trip Details 📝 ", - "select-mode-scroll": "Mode (👇 for more)", - "select-replaced-mode-scroll": "Replaces (👇 for more)", - "select-purpose-scroll": "Purpose (👇 for more)", - "delete-entry-confirm": "Are you sure you wish to delete this entry?", - "detected": "Detected:", - "labeled-mode": "Labeled Mode", - "detected-modes": "Detected Modes", - "today": "Today", - "no-more-travel": "No more travel to show", - "show-more-travel": "Show More Travel", - "show-older-travel": "Show Older Travel", - "no-travel": "No travel to show", - "no-travel-hint": "To see more, change the filters above or go record some travel!" - }, + "metrics":{ + "cancel": "Cancel", + "confirm": "Confirm", + "get": "Get", + "range": "Range", + "filter": "Filter", + "from": "From:", + "to": "To:", + "last-week": "last week", + "frequency": "Frequency:", + "pandafreqoptions-daily": "DAILY", + "pandafreqoptions-weekly": "WEEKLY", + "pandafreqoptions-biweekly": "BIWEEKLY", + "pandafreqoptions-monthly": "MONTHLY", + "pandafreqoptions-yearly": "YEARLY", + "freqoptions-daily": "DAILY", + "freqoptions-monthly": "MONTHLY", + "freqoptions-yearly": "YEARLY", + "select-pandafrequency": "Select summary freqency", + "select-frequency": "Select summary freqency", + "chart-xaxis-date": "Date", + "chart-no-data": "No Data Available", + "trips-yaxis-number": "Number", + "calorie-data-change": " change", + "calorie-data-unknown": "Unknown...", + "greater-than": " greater than ", + "greater": " greater ", + "or": "or", + "less-than": " less than ", + "less": " less ", + "week-before": "vs. week before", + "this-week": "this week", + "pick-a-date": "Pick a date", + "trips": "trips", + "hours": "hours", + "minutes": "minutes", + "custom": "Custom" + }, + + "diary": { + "distance-in-time": "{{distance}} {{distsuffix}} in {{time}}", + "distance": "Distance", + "time": "Time", + "mode": "Mode", + "replaces": "Replaces", + "purpose": "Purpose", + "survey": "Details", + "untracked-time-range": "Untracked: {{start}} - {{end}}", + "unlabeled": "All Unlabeled", + "invalid-ebike": "Invalid", + "to-label": "To Label", + "show-all": "All Trips", + "no-trips-found": "No trips found", + "choose-mode": "Mode 📝 ", + "choose-replaced-mode": "Replaces 📝", + "choose-purpose": "Purpose 📝", + "choose-survey": "Add Trip Details 📝 ", + "select-mode-scroll": "Mode (👇 for more)", + "select-replaced-mode-scroll": "Replaces (👇 for more)", + "select-purpose-scroll": "Purpose (👇 for more)", + "delete-entry-confirm": "Are you sure you wish to delete this entry?", + "detected": "Detected:", + "labeled-mode": "Labeled Mode", + "detected-modes": "Detected Modes", + "today": "Today", + "no-more-travel": "No more travel to show", + "show-more-travel": "Show More Travel", + "show-older-travel": "Show Older Travel", + "no-travel": "No travel to show", + "no-travel-hint": "To see more, change the filters above or go record some travel!" + }, - "main-metrics": { - "dashboard": "Dashboard", - "summary": "My Summary", - "chart": "Chart", - "change-data": "Change dates:", - "distance": "Distance", - "trips": "Trips", - "duration": "Duration", - "fav-mode": "My Favorite Mode", - "speed": "My Speed", - "footprint": "My Footprint", - "estimated-emissions": "Estimated CO₂ emissions", - "how-it-compares": "Ballpark comparisons", - "optimal": "Optimal (perfect mode choice for all my trips)", - "average": "Group Avg.", - "worst-case": "Worse Case", - "label-to-squish": "Label trips to collapse the range into a single number", - "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", - "lastweek": "My last week value:", - "us-2030-goal": "2030 Guideline¹", - "us-2050-goal": "2050 Guideline¹", - "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", - "past-week": "Past Week", - "prev-week": "Prev. Week", - "no-summary-data": "No summary data", - "mean-speed": "My Average Speed", - "user-totals": "My Totals", - "group-totals": "Group Totals", - "active-minutes": "Active Minutes", - "weekly-active-minutes": "Weekly minutes of active travel", - "daily-active-minutes": "Daily minutes of active travel", - "active-minutes-table": "Table of active minutes metrics", - "weekly-goal": "Weekly Goal³", - "weekly-goal-footnote": "³Weekly goal based on CDC recommendation of 150 minutes of moderate activity per week.", - "labeled": "Labeled", - "unlabeled": "Unlabeled²", - "footprint-label": "Footprint (kg CO₂)" - }, + "main-metrics":{ + "dashboard": "Dashboard", + "summary": "My Summary", + "chart": "Chart", + "change-data": "Change dates:", + "distance": "Distance", + "trips": "Trips", + "duration": "Duration", + "fav-mode": "My Favorite Mode", + "speed": "My Speed", + "footprint": "My Footprint", + "estimated-emissions": "Estimated CO₂ emissions", + "how-it-compares": "Ballpark comparisons", + "optimal": "Optimal (perfect mode choice for all my trips)", + "average": "Group Avg.", + "worst-case": "Worse Case", + "label-to-squish": "Label trips to collapse the range into a single number", + "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", + "lastweek": "My last week value:", + "us-2030-goal": "2030 Guideline¹", + "us-2050-goal": "2050 Guideline¹", + "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", + "past-week" : "Past Week", + "prev-week" : "Prev. Week", + "no-summary-data": "No summary data", + "mean-speed": "My Average Speed", + "user-totals": "My Totals", + "group-totals": "Group Totals", + "active-minutes": "Active Minutes", + "weekly-active-minutes": "Weekly minutes of active travel", + "daily-active-minutes": "Daily minutes of active travel", + "active-minutes-table": "Table of active minutes metrics", + "weekly-goal": "Weekly Goal³", + "weekly-goal-footnote": "³Weekly goal based on CDC recommendation of 150 minutes of moderate activity per week.", + "labeled": "Labeled", + "unlabeled": "Unlabeled²", + "footprint-label": "Footprint (kg CO₂)" + }, - "main-inf-scroll": { - "tab": "Label" - }, + "main-inf-scroll" : { + "tab": "Label" + }, - "details": { - "speed": "Speed", - "time": "Time" - }, + "details":{ + "speed": "Speed", + "time": "Time" + }, - "list-datepicker-today": "Today", - "list-datepicker-close": "Close", - "list-datepicker-set": "Set", + "list-datepicker-today": "Today", + "list-datepicker-close": "Close", + "list-datepicker-set": "Set", - "service": { - "reading-server": "Reading from server...", - "reading-unprocessed-data": "Reading unprocessed data..." - }, + "service":{ + "reading-server": "Reading from server...", + "reading-unprocessed-data": "Reading unprocessed data..." + }, - "email-service": { - "email-account-not-configured": "Email account is not configured, cannot send email", - "email-account-mail-app": "You must have the mail app on your phone configured with an email address. Otherwise, this won't work", - "going-to-email": "Going to email database from {{parentDir}}", - "email-log": { - "subject-logs": "emission logs", - "body-please-fill-in-what-is-wrong": "please fill in what is wrong" + "email-service":{ + "email-account-not-configured": "Email account is not configured, cannot send email", + "email-account-mail-app": "You must have the mail app on your phone configured with an email address. Otherwise, this won't work", + "going-to-email": "Going to email database from {{parentDir}}", + "email-log":{ + "subject-logs": "emission logs", + "body-please-fill-in-what-is-wrong": "please fill in what is wrong" + }, + "no-email-address-configured": "No email address configured.", + "email-data":{ + "subject-data-dump-from-to": "Data dump from {{start}} to {{end}}", + "body-data-consists-of-list-of-entries": "Data consists of a list of entries.\nEntry formats are at https://github.com/e-mission/e-mission-server/tree/master/emission/core/wrapper \nData can be loaded locally using instructions at https://github.com/e-mission/e-mission-server#loading-test-data \n and can be manipulated using the example at https://github.com/e-mission/e-mission-server/blob/master/Timeseries_Sample.ipynb" + } }, - "no-email-address-configured": "No email address configured.", - "email-data": { - "subject-data-dump-from-to": "Data dump from {{start}} to {{end}}", - "body-data-consists-of-list-of-entries": "Data consists of a list of entries.\nEntry formats are at https://github.com/e-mission/e-mission-server/tree/master/emission/core/wrapper \nData can be loaded locally using instructions at https://github.com/e-mission/e-mission-server#loading-test-data \n and can be manipulated using the example at https://github.com/e-mission/e-mission-server/blob/master/Timeseries_Sample.ipynb" - } - }, - "upload-service": { - "upload-database": "Uploading database {{db}}", - "upload-from-dir": "from directory {{parentDir}}", - "upload-to-server": "to servers {{serverURL}}", - "please-fill-in-what-is-wrong": "please fill in what is wrong", - "upload-success": "Upload successful", - "upload-progress": "Sending {{filesizemb | number}} MB to {{serverURL}}", - "upload-details": "Sent {{filesizemb | number}} MB to {{serverURL}}" - }, + "upload-service":{ + "upload-database": "Uploading database {{db}}", + "upload-from-dir": "from directory {{parentDir}}", + "upload-to-server": "to servers {{serverURL}}", + "please-fill-in-what-is-wrong": "please fill in what is wrong", + "upload-success": "Upload successful", + "upload-progress": "Sending {{filesizemb | number}} MB to {{serverURL}}", + "upload-details": "Sent {{filesizemb | number}} MB to {{serverURL}}" + }, - "intro": { - "appstatus": { - "fix": "Fix", - "refresh": "Refresh", - "overall-description": "This app works in the background to automatically build a travel diary for you. Make sure that all the settings below are green so that the app can work properly!", - "explanation-title": "What are these used for?", - "overall-loc-name": "Location", - "overall-loc-description": "We use the background location permission to track your location in the background, even when the app is closed. Reading background locations removes the need to turn tracking on and off, making the app easier to use and preventing battery drain.", - "locsettings": { - "name": "Location Settings", - "description": { - "android-lt-9": "Location services should be enabled and set to High Accuracy. This allows us to accurately record the trajectory of the travel", - "android-gte-9": "Location services should be enabled. This allows us to access location data and generate the trip log", - "ios": "Location services should be enabled. This allows us to access location data and generate the trip log" - } - }, - "locperms": { - "name": "Location Permissions", - "description": { - "android-lt-6": "Enabled during app installation.", - "android-6-9": "Please select 'allow'", - "android-10": "Please select 'Allow all the time'", - "android-11": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time'", - "android-gte-12": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time' and 'Precise'", - "ios-lt-13": "Please select 'Always allow'", - "ios-gte-13": "On the app settings page, please select 'Always' and 'Precise' and return here to continue" - } - }, - "overall-fitness-name-android": "Physical activity", - "overall-fitness-name-ios": "Motion and Fitness", - "overall-fitness-description": "The fitness sensors distinguish between walking, bicycling and motorized modes. We use this data in order to separate the parts of multi-modal travel such as transit. We also use it to as a cross-check potentially spurious trips - if the location sensor jumps across town but the fitness sensor is stationary, we can guess that the trip was invalid.", - "fitnessperms": { - "name": "Fitness Permission", - "description": { - "android": "Please allow.", - "ios": "Please allow." + "intro": { + "appstatus": { + "fix": "Fix", + "refresh":"Refresh", + "overall-description": "This app works in the background to automatically build a travel diary for you. Make sure that all the settings below are green so that the app can work properly!", + "explanation-title": "What are these used for?", + "overall-loc-name": "Location", + "overall-loc-description": "We use the background location permission to track your location in the background, even when the app is closed. Reading background locations removes the need to turn tracking on and off, making the app easier to use and preventing battery drain.", + "locsettings": { + "name": "Location Settings", + "description": { + "android-lt-9": "Location services should be enabled and set to High Accuracy. This allows us to accurately record the trajectory of the travel", + "android-gte-9": "Location services should be enabled. This allows us to access location data and generate the trip log", + "ios": "Location services should be enabled. This allows us to access location data and generate the trip log" + } + }, + "locperms": { + "name": "Location Permissions", + "description": { + "android-lt-6": "Enabled during app installation.", + "android-6-9": "Please select 'allow'", + "android-10": "Please select 'Allow all the time'", + "android-11": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time'", + "android-gte-12": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time' and 'Precise'", + "ios-lt-13": "Please select 'Always allow'", + "ios-gte-13": "On the app settings page, please select 'Always' and 'Precise' and return here to continue" + } + }, + "overall-fitness-name-android": "Physical activity", + "overall-fitness-name-ios": "Motion and Fitness", + "overall-fitness-description": "The fitness sensors distinguish between walking, bicycling and motorized modes. We use this data in order to separate the parts of multi-modal travel such as transit. We also use it to as a cross-check potentially spurious trips - if the location sensor jumps across town but the fitness sensor is stationary, we can guess that the trip was invalid.", + "fitnessperms": { + "name": "Fitness Permission", + "description": { + "android": "Please allow.", + "ios": "Please allow." + } + }, + "overall-notification-name": "Notifications", + "overall-notification-description": "We need to use notifications to inform you if the settings are incorrect. We also use hourly invisible push notifications to wake up the app and allow it to upload data and check app status. We also use notifications to remind you to label your trips.", + "notificationperms": { + "app-enabled-name": "App Notifications", + "description": { + "android-enable": "On the app settings page, ensure that all notifications and channels are enabled.", + "ios-enable": "Please allow, on the popup or the app settings page if necessary" + } + }, + "overall-background-restrictions-name": "Background restrictions", + "overall-background-restrictions-description": "The app runs in the background most of the time to make your life easier. You only need to open it periodically to label trips. Android sometimes restricts apps from working in the background. This prevents us from generating an accurate trip diary. Please remove background restrictions on this app.", + "unusedapprestrict": { + "name": "Unused apps disabled", + "description": { + "android-disable-lt-12": "On the app settings page, go to 'Permissions' and ensure that the app permissions will not be automatically reset.", + "android-disable-12": "On the app settings page, turn off 'Remove permissions and free up space.'", + "android-disable-gte-13": "On the app settings page, turn off 'Pause app activity if unused.'", + "ios": "Please allow." + } + }, + "ignorebatteryopt": { + "name": "Ignore battery optimizations", + "description": { + "android-disable": "On the optimization page, go to all apps, search for this app and turn off optimizations.", + "ios": "Please allow." + } + } + }, + "permissions": { + "locationPermExplanation-ios-lt-13": "please select 'Always allow'. This allows us to understand your travel even when you are not actively using the app", + "locationPermExplanation-ios-gte-13": "please select 'always' and 'precise' in the app settings page and return here to continue" } - }, - "overall-notification-name": "Notifications", - "overall-notification-description": "We need to use notifications to inform you if the settings are incorrect. We also use hourly invisible push notifications to wake up the app and allow it to upload data and check app status. We also use notifications to remind you to label your trips.", - "notificationperms": { - "app-enabled-name": "App Notifications", - "description": { - "android-enable": "On the app settings page, ensure that all notifications and channels are enabled.", - "ios-enable": "Please allow, on the popup or the app settings page if necessary" - } - }, - "overall-background-restrictions-name": "Background restrictions", - "overall-background-restrictions-description": "The app runs in the background most of the time to make your life easier. You only need to open it periodically to label trips. Android sometimes restricts apps from working in the background. This prevents us from generating an accurate trip diary. Please remove background restrictions on this app.", - "unusedapprestrict": { - "name": "Unused apps disabled", - "description": { - "android-disable-lt-12": "On the app settings page, go to 'Permissions' and ensure that the app permissions will not be automatically reset.", - "android-disable-12": "On the app settings page, turn off 'Remove permissions and free up space.'", - "android-disable-gte-13": "On the app settings page, turn off 'Pause app activity if unused.'", - "ios": "Please allow." - } - }, - "ignorebatteryopt": { - "name": "Ignore battery optimizations", - "description": { - "android-disable": "On the optimization page, go to all apps, search for this app and turn off optimizations.", - "ios": "Please allow." - } - } }, - "permissions": { - "locationPermExplanation-ios-lt-13": "please select 'Always allow'. This allows us to understand your travel even when you are not actively using the app", - "locationPermExplanation-ios-gte-13": "please select 'always' and 'precise' in the app settings page and return here to continue" - } - }, - "allow_background": { - "samsung": "Disable 'Medium power saving mode'" - }, - "consent": { - "permissions": "Permissions", - "button-accept": "I accept", - "button-decline": "I refuse" - }, - "login": { - "make-sure-save-your-opcode": "Make sure to save your OPcode!", - "cannot-retrieve": "NREL cannot retrieve it for you later!", - "save": "Save", - "continue": "Continue", - "enter-existing-token": "Enter the existing token that you have", - "button-accept": "OK", - "button-decline": "Cancel" - }, - "survey": { - "loading-prior-survey": "Loading prior survey responses...", - "prev-survey-found": "Found previous survey response", - "use-prior-response": "Use prior response", - "edit-response": "Edit response", - "move-on": "Move on", - "survey": "Survey", - "save": "Save", - "back": "Back", - "next": "Next", - "powered-by": "Powered by", - "dismiss": "Dismiss", - "return-to-beginning": "Return to beginning", - "go-to-end": "Go to End", - "enketo-form-errors": "Form contains errors. Please see fields marked in red.", - "enketo-timestamps-invalid": "The times you entered are invalid. Please ensure that the start time is before the end time." - }, - "join": { - "welcome-to-nrel-openpath": "Welcome to NREL OpenPATH", - "proceed-further": "To proceed further, you need to enter a valid OPcode (token)", - "what-is-opcode": "The OPcode is a long string starting with 'nrelop' that has been provided by your program admin through a website, email, text or printout.", - "or": "or", - "scan-button": "Scan the QR code ", - "scan-details": "The OPcode will be written at the top of the image", - "paste-button": "Paste the OPcode", - "paste-details": "We suggest copy-pasting instead of typing since the OPcode is long and jumbled", - "about-app-para-1": "The National Renewable Energy Laboratory’s Open Platform for Agile Trip Heuristics (NREL OpenPATH) enables people to track their travel modes—car, bus, bike, walking, etc.—and measure their associated energy use and carbon footprint.", - "about-app-para-2": "The app empowers communities to understand their travel mode choices and patterns, experiment with options to make them more sustainable, and evaluate the results. Such results can inform effective transportation policy and planning and be used to build more sustainable and accessible cities.", - "about-app-para-3": "It does so by building an automatic diary of all your trips, across all transportation modes. It reads multiple sensors, including location, in the background, and turns GPS tracking on and off automatically for minimal power consumption. The choice of the travel pattern information and the carbon footprint display style are study-specific.", - "all-green-status": "Make sure that all status checks are green", - "dont-force-kill": "Do not force kill the app", - "background-restrictions": "On Samsung and Huwaei phones, make sure that background restrictions are turned off", - "close": "Close", - "tips-title": "Tip(s) for correct operation:" - }, - "config": { - "unable-read-saved-config": "Unable to read saved config", - "unable-to-store-config": "Unable to store downladed config", - "not-enough-parts-old-style": "OPcode {{token}} does not have at least two '_' characters", - "no-nrelop-start": "OPcode {{token}} does not start with 'nrelop'", - "not-enough-parts": "OPcode {{token}} does not have at least three '_' characters", - "invalid-subgroup": "Invalid OPcode {{token}}, subgroup {{subgroup}} not found in list {{config_subgroups}}", - "invalid-subgroup-no-default": "Invalid OPcode {{token}}, no subgroups, expected 'default' subgroup", - "unable-download-config": "Unable to download study config", - "invalid-opcode-format": "Invalid OPcode format", - "error-loading-config-app-start": "Error loading config on app start", - "survey-missing-formpath": "Error while fetching resources in config: survey_info.surveys has a survey without a formPath" - }, - "errors": { - "while-populating-composite": "Error while populating composite trips", - "while-loading-another-week": "Error while loading travel of {{when}} week", - "while-loading-specific-week": "Error while loading travel for the week of {{day}}", - "while-log-messages": "While getting messages from the log ", - "while-max-index": "While getting max index " - }, - "consent-text": { - "title": "NREL OPENPATH PRIVACY POLICY/TERMS OF USE", - "introduction": { - "header": "Introduction and Purpose", - "what-is-openpath": "This data is being collected through OpenPATH, an NREL open-sourced platform. The smart phone application, NREL OpenPATH (“App”), combines data from smartphone sensors, semantic user labels and a short demographic survey.", - "what-is-NREL": "NREL is a national laboratory of the U.S. Department of Energy, Office of Energy Efficiency and Renewable Energy, operated by Alliance for Sustainable Energy, LLC under Prime Contract No. DE-AC36-08GO28308. This Privacy Policy applies to the App provided by Alliance for Sustainable Energy, LLC. This App is provided solely for the purposes of collecting travel behavior data for the {{program_or_study}} and for research to inform public policy. None of the data collected by the App will never be sold or used for any commercial purposes, including advertising.", - "if-disagree": "IF YOU DO NOT AGREE WITH THE TERMS OF THIS PRIVACY POLICY, PLEASE DELETE THE APP" + "allow_background": { + "samsung": "Disable 'Medium power saving mode'" }, - "why": { - "header": "Why we collect this information" + "consent":{ + "permissions" : "Permissions", + "button-accept": "I accept", + "button-decline": "I refuse" }, - "what": { - "header": "What information we collect", - "no-pii": "The App will never ask for any Personally Identifying Information (PII) such as name, email, address, or phone number.", - "phone-sensor": "It collects phone sensor data pertaining to your location (including background location), accelerometer, device-generated activity and mode recognition, App usage time, and battery usage. The App will create a “travel diary” based on your background location data to determine your travel patterns and location history.", - "labeling": "It will also ask you to periodically annotate these sensed trips with semantic labels, such as the trip mode, purpose, and replaced mode.", - "demographics": "It will also request sociodemographic information such as your approximate age, gender, and household type. The sociodemographic factors can be used to understand the influence of lifestyle on travel behavior, as well as generalize the results to a broader population.", - "open-source-data": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background at", - "open-source-analysis": "the analysis pipeline at", - "open-source-dashboard": "and the dashboard metrics at", - "on-nrel-site": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background, the analysis pipeline, and the dashboard metrics through links on the NREL OpenPATH website." + "login":{ + "make-sure-save-your-opcode":"Make sure to save your OPcode!", + "cannot-retrieve":"NREL cannot retrieve it for you later!", + "save":"Save", + "continue": "Continue", + "enter-existing-token": "Enter the existing token that you have", + "button-accept": "OK", + "button-decline": "Cancel" }, - "opcode": { - "header": "How we associate information with you", - "not-autogen": "Program administrators will provide you with a 'opcode' that you will use to log in to the system. This long random string will be used for all further communication with the server. If you forget or lose your opcode, you may request it by providing your name and/or email address to the program administrator. Please do not contact NREL staff with opcode retrieval requests since we do not have access to the connection between your name/email and your opcode. The data that NREL automatically collects (phone sensor data, semidemographic data, etc.) will only be associated with your 'opcode'", - "autogen": "You are logging in with a randomly generated 'opcode' that has been generated by the platform. This long random string will used for all further communication with the server. Only you know the opcode that is associated with you. There is no “Forgot password” option, and NREL staff cannot retrieve your opcode, even if you provide your name or email address. This means that, unless you store your opcode in a safe place, you will not have access to your prior data if you switch phones or uninstall and reinstall the App." + "survey": { + "loading-prior-survey": "Loading prior survey responses...", + "prev-survey-found": "Found previous survey response", + "use-prior-response": "Use prior response", + "edit-response": "Edit response", + "move-on": "Move on", + "survey": "Survey", + "save": "Save", + "back": "Back", + "next": "Next", + "powered-by": "Powered by", + "dismiss": "Dismiss", + "return-to-beginning": "Return to beginning", + "go-to-end": "Go to End", + "enketo-form-errors": "Form contains errors. Please see fields marked in red.", + "enketo-timestamps-invalid": "The times you entered are invalid. Please ensure that the start time is before the end time." }, - "who-sees": { - "header": "Who gets to see the information", - "public-dash": "Aggregate metrics derived from the travel patterns will be made available on a public dashboard to provide transparency into the impact of the program. These metrics will focus on information summaries such as counts, distances and durations, and will not display individual travel locations or times.", - "individual-info": "Individual labeling rates and trip level information will only be made available to:", - "program-admins": "🧑 Program administrators from {{deployment_partner_name}} to {{raw_data_use}}, and", - "nrel-devs": "💻 NREL OpenPATH developers for debugging", - "TSDC-info": "The data will also be periodically archived in NREL’s Transportation Secure Data Center (TSDC) after a delay of 3 to 6 months. It will then be made available for legitimate research through existing, privacy-preserving TSDC operating procedures. Further information on the procedures is available", - "on-website": " on the website ", - "and-in": "and in", - "this-pub": " this publication ", - "and": "and", - "fact-sheet": " fact sheet", - "on-nrel-site": " through links on the NREL OpenPATH website." + "join": { + "welcome-to-nrel-openpath": "Welcome to NREL OpenPATH", + "proceed-further": "To proceed further, you need to enter a valid OPcode (token)", + "what-is-opcode": "The OPcode is a long string starting with 'nrelop' that has been provided by your program admin through a website, email, text or printout.", + "or": "or", + "scan-button": "Scan the QR code ", + "scan-details": "The OPcode will be written at the top of the image", + "paste-button": "Paste the OPcode", + "paste-details": "We suggest copy-pasting instead of typing since the OPcode is long and jumbled", + "about-app-para-1": "The National Renewable Energy Laboratory’s Open Platform for Agile Trip Heuristics (NREL OpenPATH) enables people to track their travel modes—car, bus, bike, walking, etc.—and measure their associated energy use and carbon footprint.", + "about-app-para-2": "The app empowers communities to understand their travel mode choices and patterns, experiment with options to make them more sustainable, and evaluate the results. Such results can inform effective transportation policy and planning and be used to build more sustainable and accessible cities.", + "about-app-para-3": "It does so by building an automatic diary of all your trips, across all transportation modes. It reads multiple sensors, including location, in the background, and turns GPS tracking on and off automatically for minimal power consumption. The choice of the travel pattern information and the carbon footprint display style are study-specific.", + "all-green-status": "Make sure that all status checks are green", + "dont-force-kill": "Do not force kill the app", + "background-restrictions": "On Samsung and Huwaei phones, make sure that background restrictions are turned off", + "close": "Close", + "tips-title": "Tip(s) for correct operation:" }, - "rights": { - "header": "Your rights", - "app-required": "You are required to track your travel patterns using the App as a condition of participation in the Program. If you wish to withdraw from the Program, you should contact the program administrator, {{program_admin_contact}} to discuss termination options. If you wish to stay in the program but not use the app, please contact your program administrator to negotiate an alternative data collection procedure before uninstalling the app. If you uninstall the app without approval from the program administrator, you may not have access to the benefits provided by the program.", - "app-not-required": "Participation in the {{program_or_study}} is completely voluntary. You have the right to decline to participate or to withdraw at any point in the Study without providing notice to NREL or the point of contact. If you do not wish to participate in the Study or to discontinue your participation in the Study, please delete the App.", - "destroy-data-pt1": "If you would like to have your data destroyed, please contact K. Shankari ", - "destroy-data-pt2": " requesting deletion. You must include your token in the request for deletion. Because we do not connect your identity with your token, we cannot delete your information without obtaining the token as part of the deletion request. We will then destroy all data associated with that deletion request, both in the online and archived datasets." + "config": { + "unable-read-saved-config": "Unable to read saved config", + "unable-to-store-config": "Unable to store downladed config", + "not-enough-parts-old-style": "OPcode {{token}} does not have at least two '_' characters", + "no-nrelop-start": "OPcode {{token}} does not start with 'nrelop'", + "not-enough-parts": "OPcode {{token}} does not have at least three '_' characters", + "invalid-subgroup": "Invalid OPcode {{token}}, subgroup {{subgroup}} not found in list {{config_subgroups}}", + "invalid-subgroup-no-default": "Invalid OPcode {{token}}, no subgroups, expected 'default' subgroup", + "unable-download-config": "Unable to download study config", + "invalid-opcode-format": "Invalid OPcode format", + "error-loading-config-app-start": "Error loading config on app start", + "survey-missing-formpath": "Error while fetching resources in config: survey_info.surveys has a survey without a formPath" }, - "questions": { - "header": "Questions", - "for-questions": "If you have any questions about the data collection goals and results, please contact the primary point of contact for the study, {{program_admin_contact}}. If you have any technical questions about app operation, please contact NREL’s K. Shankari (k.shankari@nrel.gov)." + "errors": { + "while-populating-composite": "Error while populating composite trips", + "while-loading-another-week": "Error while loading travel of {{when}} week", + "while-loading-specific-week": "Error while loading travel for the week of {{day}}", + "while-log-messages": "While getting messages from the log ", + "while-max-index" : "While getting max index " }, - "consent": { - "header": "Consent", - "press-button-to-consent": "Please select the button below to indicate that you have read and agree to this Privacy Policy, consent to the collection of your information, and want to participate in the {{program_or_study}}." + "consent-text": { + "title":"NREL OPENPATH PRIVACY POLICY/TERMS OF USE", + "introduction":{ + "header":"Introduction and Purpose", + "what-is-openpath":"This data is being collected through OpenPATH, an NREL open-sourced platform. The smart phone application, NREL OpenPATH (“App”), combines data from smartphone sensors, semantic user labels and a short demographic survey.", + "what-is-NREL":"NREL is a national laboratory of the U.S. Department of Energy, Office of Energy Efficiency and Renewable Energy, operated by Alliance for Sustainable Energy, LLC under Prime Contract No. DE-AC36-08GO28308. This Privacy Policy applies to the App provided by Alliance for Sustainable Energy, LLC. This App is provided solely for the purposes of collecting travel behavior data for the {{program_or_study}} and for research to inform public policy. None of the data collected by the App will never be sold or used for any commercial purposes, including advertising.", + "if-disagree":"IF YOU DO NOT AGREE WITH THE TERMS OF THIS PRIVACY POLICY, PLEASE DELETE THE APP" + }, + "why":{ + "header":"Why we collect this information" + }, + "what":{ + "header":"What information we collect", + "no-pii":"The App will never ask for any Personally Identifying Information (PII) such as name, email, address, or phone number.", + "phone-sensor":"It collects phone sensor data pertaining to your location (including background location), accelerometer, device-generated activity and mode recognition, App usage time, and battery usage. The App will create a “travel diary” based on your background location data to determine your travel patterns and location history.", + "labeling":"It will also ask you to periodically annotate these sensed trips with semantic labels, such as the trip mode, purpose, and replaced mode.", + "demographics":"It will also request sociodemographic information such as your approximate age, gender, and household type. The sociodemographic factors can be used to understand the influence of lifestyle on travel behavior, as well as generalize the results to a broader population.", + "open-source-data":"For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background at", + "open-source-analysis":"the analysis pipeline at", + "open-source-dashboard":"and the dashboard metrics at", + "on-nrel-site": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background, the analysis pipeline, and the dashboard metrics through links on the NREL OpenPATH website." + }, + "opcode":{ + "header":"How we associate information with you", + "not-autogen":"Program administrators will provide you with a 'opcode' that you will use to log in to the system. This long random string will be used for all further communication with the server. If you forget or lose your opcode, you may request it by providing your name and/or email address to the program administrator. Please do not contact NREL staff with opcode retrieval requests since we do not have access to the connection between your name/email and your opcode. The data that NREL automatically collects (phone sensor data, semidemographic data, etc.) will only be associated with your 'opcode'", + "autogen":"You are logging in with a randomly generated 'opcode' that has been generated by the platform. This long random string will used for all further communication with the server. Only you know the opcode that is associated with you. There is no “Forgot password” option, and NREL staff cannot retrieve your opcode, even if you provide your name or email address. This means that, unless you store your opcode in a safe place, you will not have access to your prior data if you switch phones or uninstall and reinstall the App." + }, + "who-sees":{ + "header":"Who gets to see the information", + "public-dash":"Aggregate metrics derived from the travel patterns will be made available on a public dashboard to provide transparency into the impact of the program. These metrics will focus on information summaries such as counts, distances and durations, and will not display individual travel locations or times.", + "individual-info":"Individual labeling rates and trip level information will only be made available to:", + "program-admins":"🧑 Program administrators from {{deployment_partner_name}} to {{raw_data_use}}, and", + "nrel-devs":"💻 NREL OpenPATH developers for debugging", + "TSDC-info":"The data will also be periodically archived in NREL’s Transportation Secure Data Center (TSDC) after a delay of 3 to 6 months. It will then be made available for legitimate research through existing, privacy-preserving TSDC operating procedures. Further information on the procedures is available", + "on-website":" on the website ", + "and-in":"and in", + "this-pub":" this publication ", + "and":"and", + "fact-sheet":" fact sheet", + "on-nrel-site": " through links on the NREL OpenPATH website." + }, + "rights":{ + "header":"Your rights", + "app-required":"You are required to track your travel patterns using the App as a condition of participation in the Program. If you wish to withdraw from the Program, you should contact the program administrator, {{program_admin_contact}} to discuss termination options. If you wish to stay in the program but not use the app, please contact your program administrator to negotiate an alternative data collection procedure before uninstalling the app. If you uninstall the app without approval from the program administrator, you may not have access to the benefits provided by the program.", + "app-not-required":"Participation in the {{program_or_study}} is completely voluntary. You have the right to decline to participate or to withdraw at any point in the Study without providing notice to NREL or the point of contact. If you do not wish to participate in the Study or to discontinue your participation in the Study, please delete the App.", + "destroy-data-pt1":"If you would like to have your data destroyed, please contact K. Shankari ", + "destroy-data-pt2":" requesting deletion. You must include your token in the request for deletion. Because we do not connect your identity with your token, we cannot delete your information without obtaining the token as part of the deletion request. We will then destroy all data associated with that deletion request, both in the online and archived datasets." + }, + "questions":{ + "header":"Questions", + "for-questions":"If you have any questions about the data collection goals and results, please contact the primary point of contact for the study, {{program_admin_contact}}. If you have any technical questions about app operation, please contact NREL’s K. Shankari (k.shankari@nrel.gov)." + }, + "consent":{ + "header":"Consent", + "press-button-to-consent":"Please select the button below to indicate that you have read and agree to this Privacy Policy, consent to the collection of your information, and want to participate in the {{program_or_study}}." + } } - } } diff --git a/www/js/angular-react-helper.tsx b/www/js/angular-react-helper.tsx index eefbcd6e5..984e529ff 100644 --- a/www/js/angular-react-helper.tsx +++ b/www/js/angular-react-helper.tsx @@ -5,29 +5,27 @@ import angular from 'angular'; import { createRoot } from 'react-dom/client'; import React from 'react'; -import { - Provider as PaperProvider, - MD3LightTheme as DefaultTheme, - MD3Colors, -} from 'react-native-paper'; +import { Provider as PaperProvider, MD3LightTheme as DefaultTheme, MD3Colors } from 'react-native-paper'; import { getTheme } from './appTheme'; function toBindings(propTypes) { const bindings = {}; - Object.keys(propTypes).forEach((key) => (bindings[key] = '<')); + Object.keys(propTypes).forEach(key => bindings[key] = '<'); return bindings; } function toProps(propTypes, controller) { const props = {}; - Object.keys(propTypes).forEach((key) => (props[key] = controller[key])); + Object.keys(propTypes).forEach(key => props[key] = controller[key]); return props; } export function angularize(component, name, modulePath) { component.module = modulePath; const nameCamelCase = name[0].toLowerCase() + name.slice(1); - angular.module(modulePath, []).component(nameCamelCase, makeComponentProps(component)); + angular + .module(modulePath, []) + .component(nameCamelCase, makeComponentProps(component)); } const theme = getTheme(); @@ -35,33 +33,29 @@ export function makeComponentProps(Component) { const propTypes = Component.propTypes || {}; return { bindings: toBindings(propTypes), - controller: [ - '$element', - function ($element) { - /* TODO: once the inf scroll list is converted to React and no longer uses + controller: ['$element', function($element) { + /* TODO: once the inf scroll list is converted to React and no longer uses collection-repeat, we can just set the root here one time and will not have to reassign it in $onChanges. */ - /* Until then, React will complain everytime we reassign an element's root */ - let root; - this.$onChanges = () => { - root = createRoot($element[0]); - const props = toProps(propTypes, this); - root.render( - - - - , - ); - }; - this.$onDestroy = () => root.unmount(); - }, - ], + + + + ); + }; + this.$onDestroy = () => root.unmount(); + }] }; } @@ -76,11 +70,11 @@ export function getAngularService(name: string) { throw new Error(`Couldn't find "${name}" angular service`); } - return service as any; // casting to 'any' because not all Angular services are typed + return (service as any); // casting to 'any' because not all Angular services are typed } export function createScopeWithVars(vars) { - const scope = getAngularService('$rootScope').$new(); + const scope = getAngularService("$rootScope").$new(); Object.assign(scope, vars); return scope; } diff --git a/www/js/app.js b/www/js/app.js index 7ebbb90c5..a52edaa12 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -25,122 +25,100 @@ import initializedI18next from './i18nextInit'; window.i18next = initializedI18next; import 'ng-i18next'; -angular - .module('emission', [ - 'ionic', - 'jm.i18next', - 'emission.controllers', - 'emission.services', - 'emission.plugin.logger', - 'emission.splash.customURLScheme', - 'emission.splash.referral', +angular.module('emission', ['ionic', 'jm.i18next', + 'emission.controllers','emission.services', 'emission.plugin.logger', + 'emission.splash.customURLScheme', 'emission.splash.referral', 'emission.services.email', - 'emission.intro', - 'emission.main', - 'emission.config.dynamic', - 'emission.config.server_conn', - 'emission.join.ctrl', - 'pascalprecht.translate', - 'LocalStorageModule', - ]) - - .run( - function ( - $ionicPlatform, - $rootScope, - $http, - Logger, - CustomURLScheme, - ReferralHandler, - DynamicConfig, - localStorageService, - ServerConnConfig, - ) { - console.log('Starting run'); - // ensure that plugin events are delivered after the ionicPlatform is ready - // https://github.com/katzer/cordova-plugin-local-notifications#launch-details - window.skipLocalNotificationReady = true; - // alert("Starting run"); - // BEGIN: Global listeners, no need to wait for the platform - // TODO: Although the onLaunch call doesn't need to wait for the platform the - // handlers do. Can we rely on the fact that the event is generated from - // native code, so will only be launched after the platform is ready? - CustomURLScheme.onLaunch(function (event, url, urlComponents) { - console.log('GOT URL:' + url); - // alert("GOT URL:"+url); - - if (urlComponents.route == 'join') { - ReferralHandler.setupGroupReferral(urlComponents); - StartPrefs.loadWithPrefs(); - } else if (urlComponents.route == 'login_token') { - DynamicConfig.initByUser(urlComponents); - } - }); - // END: Global listeners - $ionicPlatform.ready(function () { - // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard - // for form inputs) - Logger.log('ionicPlatform is ready'); - - if (window.StatusBar) { - // org.apache.cordova.statusbar required - StatusBar.styleDefault(); - } - cordova.plugin.http.setDataSerializer('json'); - // backwards compat hack to be consistent with - // https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7#diff-962320754eba07107ecd413954411f725c98fd31cddbb5defd4a542d1607e5a3R160 - // remove during migration to react native - localStorageService.remove('OP_GEOFENCE_CFG'); - cordova.plugins.BEMUserCache.removeLocalStorage('OP_GEOFENCE_CFG'); - }); - console.log('Ending run'); - }, - ) - - .config(function ($stateProvider, $urlRouterProvider, $compileProvider) { - console.log('Starting config'); - // alert("config"); - - // Ionic uses AngularUI Router which uses the concept of states - // Learn more here: https://github.com/angular-ui/ui-router - // Set a few states which the app can be in. - // The 'intro' and 'diary' states are found in their respective modules - // Each state's controller can be found in controllers.js - $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|ionic):|data:image/); - $stateProvider - // set up a state for the splash screen. This has no parents and no children - // because it is basically just used to load the user's preferred screen. - // This cannot directly use plugins - has to check for them first. - .state('splash', { + 'emission.intro', 'emission.main', 'emission.config.dynamic', + 'emission.config.server_conn', 'emission.join.ctrl', + 'pascalprecht.translate', 'LocalStorageModule']) + +.run(function($ionicPlatform, $rootScope, $http, Logger, + CustomURLScheme, ReferralHandler, DynamicConfig, localStorageService, ServerConnConfig) { + console.log("Starting run"); + // ensure that plugin events are delivered after the ionicPlatform is ready + // https://github.com/katzer/cordova-plugin-local-notifications#launch-details + window.skipLocalNotificationReady = true; + // alert("Starting run"); + // BEGIN: Global listeners, no need to wait for the platform + // TODO: Although the onLaunch call doesn't need to wait for the platform the + // handlers do. Can we rely on the fact that the event is generated from + // native code, so will only be launched after the platform is ready? + CustomURLScheme.onLaunch(function(event, url, urlComponents){ + console.log("GOT URL:"+url); + // alert("GOT URL:"+url); + + if (urlComponents.route == 'join') { + ReferralHandler.setupGroupReferral(urlComponents); + StartPrefs.loadWithPrefs(); + } else if (urlComponents.route == 'login_token') { + DynamicConfig.initByUser(urlComponents); + } + }); + // END: Global listeners + $ionicPlatform.ready(function() { + // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard + // for form inputs) + Logger.log("ionicPlatform is ready"); + + if (window.StatusBar) { + // org.apache.cordova.statusbar required + StatusBar.styleDefault(); + } + cordova.plugin.http.setDataSerializer('json'); + // backwards compat hack to be consistent with + // https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7#diff-962320754eba07107ecd413954411f725c98fd31cddbb5defd4a542d1607e5a3R160 + // remove during migration to react native + localStorageService.remove("OP_GEOFENCE_CFG"); + cordova.plugins.BEMUserCache.removeLocalStorage("OP_GEOFENCE_CFG"); + }); + console.log("Ending run"); +}) + +.config(function($stateProvider, $urlRouterProvider, $compileProvider) { + console.log("Starting config"); + // alert("config"); + + // Ionic uses AngularUI Router which uses the concept of states + // Learn more here: https://github.com/angular-ui/ui-router + // Set a few states which the app can be in. + // The 'intro' and 'diary' states are found in their respective modules + // Each state's controller can be found in controllers.js + $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|ionic):|data:image/); + $stateProvider + // set up a state for the splash screen. This has no parents and no children + // because it is basically just used to load the user's preferred screen. + // This cannot directly use plugins - has to check for them first. + .state('splash', { url: '/splash', templateUrl: 'templates/splash/splash.html', - controller: 'SplashCtrl', - }) - - // add the join screen to the list of initially defined states - // we can't put it in intro since it comes before it - // we can't put it in main because it is also a temporary screen that only - // shows up when we have no config. - // so we put it in here - .state('root.join', { - url: '/join', - templateUrl: 'templates/join/request_join.html', - controller: 'JoinCtrl', - }) - - // setup an abstract state for the root. Only children of this can be loaded - // as preferred screens, and all children of this can assume that the device - // is ready. - .state('root', { - url: '/root', - abstract: true, - template: '', - controller: 'RootCtrl', - }); - - // alert("about to fall back to otherwise"); - // if none of the above states are matched, use this as the fallback - $urlRouterProvider.otherwise('/splash'); - - console.log('Ending config'); + controller: 'SplashCtrl' + }) + + // add the join screen to the list of initially defined states + // we can't put it in intro since it comes before it + // we can't put it in main because it is also a temporary screen that only + // shows up when we have no config. + // so we put it in here + .state('root.join', { + url: '/join', + templateUrl: 'templates/join/request_join.html', + controller: 'JoinCtrl' + }) + + // setup an abstract state for the root. Only children of this can be loaded + // as preferred screens, and all children of this can assume that the device + // is ready. + .state('root', { + url: '/root', + abstract: true, + template: '', + controller: 'RootCtrl' }); + + // alert("about to fall back to otherwise"); + // if none of the above states are matched, use this as the fallback + $urlRouterProvider.otherwise('/splash'); + + console.log("Ending config"); +}); diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 0a6834534..5f47f00b1 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -28,7 +28,7 @@ const AppTheme = { }, success: '#00a665', // lch(60% 55 155) warn: '#f8cf53', //lch(85% 65 85) - danger: '#f23934', // lch(55% 85 35) + danger: '#f23934' // lch(55% 85 35) }, roundness: 5, }; @@ -47,26 +47,23 @@ type DPartial = { [P in keyof T]?: DPartial }; // https://stackoverflow type PartialTheme = DPartial; const flavorOverrides = { - place: { - // for PlaceCards; a blueish color scheme + place: { // for PlaceCards; a blueish color scheme colors: { elevation: { level1: '#cbe6ff', // lch(90, 20, 250) }, - }, + } }, - untracked: { - // for UntrackedTimeCards; a reddish color scheme + untracked: { // for UntrackedTimeCards; a reddish color scheme colors: { primary: '#8c4a57', // lch(40 30 10) primaryContainer: '#e3bdc2', // lch(80 15 10) elevation: { level1: '#f8ebec', // lch(94 5 10) }, - }, + } }, - draft: { - // for TripCards and LabelDetailsScreen of draft trips; a greyish color scheme + draft: { // for TripCards and LabelDetailsScreen of draft trips; a greyish color scheme colors: { primary: '#616971', // lch(44 6 250) primaryContainer: '#b6bcc2', // lch(76 4 250) @@ -77,7 +74,7 @@ const flavorOverrides = { level1: '#e1e3e4', // lch(90 1 250) level2: '#d2d5d8', // lch(85 2 250) }, - }, + } }, } satisfies Record; @@ -86,10 +83,7 @@ const flavorOverrides = { export const getTheme = (flavor?: keyof typeof flavorOverrides) => { if (!flavorOverrides[flavor]) return AppTheme; const typeStyle = flavorOverrides[flavor]; - const scopedElevation = { ...AppTheme.colors.elevation, ...typeStyle?.colors?.elevation }; - const scopedColors = { - ...AppTheme.colors, - ...{ ...typeStyle.colors, elevation: scopedElevation }, - }; - return { ...AppTheme, colors: scopedColors }; -}; + const scopedElevation = {...AppTheme.colors.elevation, ...typeStyle?.colors?.elevation}; + const scopedColors = {...AppTheme.colors, ...{...typeStyle.colors, elevation: scopedElevation}}; + return {...AppTheme, colors: scopedColors}; +} diff --git a/www/js/appstatus/ExplainPermissions.tsx b/www/js/appstatus/ExplainPermissions.tsx index d0d63ebe7..cb0db4bba 100644 --- a/www/js/appstatus/ExplainPermissions.tsx +++ b/www/js/appstatus/ExplainPermissions.tsx @@ -1,34 +1,41 @@ -import React from 'react'; -import { Modal, ScrollView, useWindowDimensions, View } from 'react-native'; +import React from "react"; +import { Modal, ScrollView, useWindowDimensions, View } from "react-native"; import { Button, Dialog, Text } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; const ExplainPermissions = ({ explanationList, visible, setVisible }) => { - const { t } = useTranslation(); - const { height: windowHeight } = useWindowDimensions(); + const { t } = useTranslation(); + const { height: windowHeight } = useWindowDimensions(); - return ( - setVisible(false)}> - setVisible(false)}> - {t('intro.appstatus.explanation-title')} - - - {explanationList?.map((li) => ( - - - {li.name} - - {li.desc} - - ))} - - - - - - - - ); + return ( + setVisible(false)} > + setVisible(false)} > + {t('intro.appstatus.explanation-title')} + + + {explanationList?.map((li) => + + + {li.name} + + + {li.desc} + + + )} + + + + + + + + ); }; -export default ExplainPermissions; +export default ExplainPermissions; \ No newline at end of file diff --git a/www/js/appstatus/PermissionItem.tsx b/www/js/appstatus/PermissionItem.tsx index cd111f3b3..2899943f1 100644 --- a/www/js/appstatus/PermissionItem.tsx +++ b/www/js/appstatus/PermissionItem.tsx @@ -1,19 +1,21 @@ -import React from 'react'; +import React from "react"; import { List, Button } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; const PermissionItem = ({ check }) => { - const { t } = useTranslation(); + const { t } = useTranslation(); - return ( - } - right={() => } - /> - ); + return ( + } + right={() => } + /> + ); }; - -export default PermissionItem; + +export default PermissionItem; \ No newline at end of file diff --git a/www/js/appstatus/permissioncheck.js b/www/js/appstatus/permissioncheck.js index 95d4f576c..84067a701 100644 --- a/www/js/appstatus/permissioncheck.js +++ b/www/js/appstatus/permissioncheck.js @@ -4,538 +4,426 @@ import angular from 'angular'; -angular - .module('emission.appstatus.permissioncheck', []) - .directive('permissioncheck', function () { +angular.module('emission.appstatus.permissioncheck', + []) +.directive('permissioncheck', function() { return { - scope: { - overallstatus: '=', - }, - controller: 'PermissionCheckControl', - templateUrl: 'templates/appstatus/permissioncheck.html', + scope: { + overallstatus: "=", + }, + controller: "PermissionCheckControl", + templateUrl: "templates/appstatus/permissioncheck.html" }; - }) - .controller( - 'PermissionCheckControl', - function ($scope, $element, $attrs, $ionicPlatform, $ionicPopup, $window) { - console.log('PermissionCheckControl initialized with status ' + $scope.overallstatus); +}). +controller("PermissionCheckControl", function($scope, $element, $attrs, + $ionicPlatform, $ionicPopup, $window) { + console.log("PermissionCheckControl initialized with status "+$scope.overallstatus); - $scope.setupLocChecks = function (platform, version) { - if (platform.toLowerCase() == 'android') { - return $scope.setupAndroidLocChecks(version); - } else if (platform.toLowerCase() == 'ios') { - return $scope.setupIOSLocChecks(version); + $scope.setupLocChecks = function(platform, version) { + if (platform.toLowerCase() == "android") { + return $scope.setupAndroidLocChecks(version); + } else if (platform.toLowerCase() == "ios") { + return $scope.setupIOSLocChecks(version); } else { - alert('Unknown platform, no tracking'); + alert("Unknown platform, no tracking"); } - }; + } - $scope.setupFitnessChecks = function (platform, version) { - if (platform.toLowerCase() == 'android') { - return $scope.setupAndroidFitnessChecks(version); - } else if (platform.toLowerCase() == 'ios') { - return $scope.setupIOSFitnessChecks(version); + $scope.setupFitnessChecks = function(platform, version) { + if (platform.toLowerCase() == "android") { + return $scope.setupAndroidFitnessChecks(version); + } else if (platform.toLowerCase() == "ios") { + return $scope.setupIOSFitnessChecks(version); } else { - alert('Unknown platform, no tracking'); + alert("Unknown platform, no tracking"); } - }; + } - $scope.setupNotificationChecks = function (platform, version) { - return $scope.setupAndroidNotificationChecks(version); - }; + $scope.setupNotificationChecks = function(platform, version) { + return $scope.setupAndroidNotificationChecks(version); + } - $scope.setupBackgroundRestrictionChecks = function (platform, version) { - if (platform.toLowerCase() == 'android') { - $scope.backgroundUnrestrictionsNeeded = true; - return $scope.setupAndroidBackgroundRestrictionChecks(version); - } else if (platform.toLowerCase() == 'ios') { - $scope.backgroundUnrestrictionsNeeded = false; - $scope.overallBackgroundRestrictionStatus = true; - $scope.backgroundRestrictionChecks = []; - return true; + $scope.setupBackgroundRestrictionChecks = function(platform, version) { + if (platform.toLowerCase() == "android") { + $scope.backgroundUnrestrictionsNeeded = true; + return $scope.setupAndroidBackgroundRestrictionChecks(version); + } else if (platform.toLowerCase() == "ios") { + $scope.backgroundUnrestrictionsNeeded = false; + $scope.overallBackgroundRestrictionStatus = true; + $scope.backgroundRestrictionChecks = []; + return true; } else { - alert('Unknown platform, no tracking'); + alert("Unknown platform, no tracking"); } - }; + } - let iconMap = (statusState) => (statusState ? '✅' : '❌'); - let classMap = (statusState) => (statusState ? 'status-green' : 'status-red'); + let iconMap = (statusState) => statusState? "✅" : "❌"; + let classMap = (statusState) => statusState? "status-green" : "status-red"; - $scope.recomputeOverallStatus = function () { - $scope.overallstatus = - $scope.overallLocStatus && - $scope.overallFitnessStatus && - $scope.overallNotificationStatus && - $scope.overallBackgroundRestrictionStatus; - }; + $scope.recomputeOverallStatus = function() { + $scope.overallstatus = $scope.overallLocStatus + && $scope.overallFitnessStatus + && $scope.overallNotificationStatus + && $scope.overallBackgroundRestrictionStatus; + } - $scope.recomputeLocStatus = function () { + $scope.recomputeLocStatus = function() { $scope.locChecks.forEach((lc) => { - lc.statusIcon = iconMap(lc.statusState); - lc.statusClass = classMap(lc.statusState); + lc.statusIcon = iconMap(lc.statusState); + lc.statusClass = classMap(lc.statusState) }); - $scope.overallLocStatus = $scope.locChecks - .map((lc) => lc.statusState) - .reduce((pv, cv) => pv && cv); - console.log('overallLocStatus = ' + $scope.overallLocStatus + ' from ', $scope.locChecks); + $scope.overallLocStatus = $scope.locChecks.map((lc) => lc.statusState).reduce((pv, cv) => pv && cv); + console.log("overallLocStatus = "+$scope.overallLocStatus+" from ", $scope.locChecks); $scope.overallLocStatusIcon = iconMap($scope.overallLocStatus); $scope.overallLocStatusClass = classMap($scope.overallLocStatus); $scope.recomputeOverallStatus(); - }; + } - $scope.recomputeFitnessStatus = function () { + $scope.recomputeFitnessStatus = function() { $scope.fitnessChecks.forEach((fc) => { - fc.statusIcon = iconMap(fc.statusState); - fc.statusClass = classMap(fc.statusState); + fc.statusIcon = iconMap(fc.statusState); + fc.statusClass = classMap(fc.statusState) }); - $scope.overallFitnessStatus = $scope.fitnessChecks - .map((fc) => fc.statusState) - .reduce((pv, cv) => pv && cv); - console.log( - 'overallFitnessStatus = ' + $scope.overallFitnessStatus + ' from ', - $scope.fitnessChecks, - ); + $scope.overallFitnessStatus = $scope.fitnessChecks.map((fc) => fc.statusState).reduce((pv, cv) => pv && cv); + console.log("overallFitnessStatus = "+$scope.overallFitnessStatus+" from ", $scope.fitnessChecks); $scope.overallFitnessStatusIcon = iconMap($scope.overallFitnessStatus); $scope.overallFitnessStatusClass = classMap($scope.overallFitnessStatus); $scope.recomputeOverallStatus(); - }; + } - $scope.recomputeNotificationStatus = function () { + $scope.recomputeNotificationStatus = function() { $scope.notificationChecks.forEach((nc) => { - nc.statusIcon = iconMap(nc.statusState); - nc.statusClass = classMap(nc.statusState); + nc.statusIcon = iconMap(nc.statusState); + nc.statusClass = classMap(nc.statusState) }); - $scope.overallNotificationStatus = $scope.notificationChecks - .map((nc) => nc.statusState) - .reduce((pv, cv) => pv && cv); - console.log( - 'overallNotificationStatus = ' + $scope.overallNotificationStatus + ' from ', - $scope.notificationChecks, - ); + $scope.overallNotificationStatus = $scope.notificationChecks.map((nc) => nc.statusState).reduce((pv, cv) => pv && cv); + console.log("overallNotificationStatus = "+$scope.overallNotificationStatus+" from ", $scope.notificationChecks); $scope.overallNotificationStatusIcon = iconMap($scope.overallNotificationStatus); $scope.overallNotificationStatusClass = classMap($scope.overallNotificationStatus); $scope.recomputeOverallStatus(); - }; + } - $scope.recomputeBackgroundRestrictionStatus = function () { + $scope.recomputeBackgroundRestrictionStatus = function() { if (!$scope.backgroundRestrictionChecks) return; $scope.backgroundRestrictionChecks.forEach((brc) => { - brc.statusIcon = iconMap(brc.statusState); - brc.statusClass = classMap(brc.statusState); + brc.statusIcon = iconMap(brc.statusState); + brc.statusClass = classMap(brc.statusState) }); - $scope.overallBackgroundRestrictionStatus = $scope.backgroundRestrictionChecks - .map((nc) => nc.statusState) - .reduce((pv, cv) => pv && cv); - console.log( - 'overallBackgroundRestrictionStatus = ' + - $scope.overallBackgroundRestrictionStatus + - ' from ', - $scope.backgroundRestrictionChecks, - ); - $scope.overallBackgroundRestrictionStatusIcon = iconMap( - $scope.overallBackgroundRestrictionStatus, - ); - $scope.overallBackgroundRestrictionStatusClass = classMap( - $scope.overallBackgroundRestrictionStatus, - ); + $scope.overallBackgroundRestrictionStatus = $scope.backgroundRestrictionChecks.map((nc) => nc.statusState).reduce((pv, cv) => pv && cv); + console.log("overallBackgroundRestrictionStatus = "+$scope.overallBackgroundRestrictionStatus+" from ", $scope.backgroundRestrictionChecks); + $scope.overallBackgroundRestrictionStatusIcon = iconMap($scope.overallBackgroundRestrictionStatus); + $scope.overallBackgroundRestrictionStatusClass = classMap($scope.overallBackgroundRestrictionStatus); $scope.recomputeOverallStatus(); - }; + } - let checkOrFix = function (checkObj, nativeFn, recomputeFn, showError = true) { + let checkOrFix = function(checkObj, nativeFn, recomputeFn, showError=true) { return nativeFn() - .then((status) => { - console.log('availability ', status); - $scope.$apply(() => { - checkObj.statusState = true; - recomputeFn(); + .then((status) => { + console.log("availability ", status) + $scope.$apply(() => { + checkObj.statusState = true; + recomputeFn(); + }); + return status; + }).catch((error) => { + console.log("Error", error) + if (showError) { + $ionicPopup.alert({ + title: "Error", + template: "
"+error+"
", + okText: "Please fix again" + }); + }; + $scope.$apply(() => { + checkObj.statusState = false; + recomputeFn(); + }); + return error; }); - return status; - }) - .catch((error) => { - console.log('Error', error); - if (showError) { - $ionicPopup.alert({ - title: 'Error', - template: "
" + error + '
', - okText: 'Please fix again', - }); - } - $scope.$apply(() => { - checkObj.statusState = false; - recomputeFn(); - }); - return error; - }); - }; + } - let refreshChecks = function (checksList, recomputeFn) { + let refreshChecks = function(checksList, recomputeFn) { // without this, even if the checksList is [] // the reduce in the recomputeFn fails because it is called on a zero // length array without a default value // we should be able to also specify a default value of True // but I don't want to mess with that at this last minute if (!checksList || checksList.length == 0) { - return Promise.resolve(true); + return Promise.resolve(true); } let checkPromises = checksList?.map((lc) => lc.refresh()); console.log(checkPromises); return Promise.all(checkPromises) - .then((result) => recomputeFn()) - .catch((error) => recomputeFn()); - }; + .then((result) => recomputeFn()) + .catch((error) => recomputeFn()) + } - $scope.setupAndroidLocChecks = function (platform, version) { - let fixSettings = function () { - console.log('Fix and refresh location settings'); - return checkOrFix( - locSettingsCheck, - $window.cordova.plugins.BEMDataCollection.fixLocationSettings, - $scope.recomputeLocStatus, - true, - ); + $scope.setupAndroidLocChecks = function(platform, version) { + let fixSettings = function() { + console.log("Fix and refresh location settings"); + return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationSettings, + $scope.recomputeLocStatus, true); }; - let checkSettings = function () { - console.log('Refresh location settings'); - return checkOrFix( - locSettingsCheck, - $window.cordova.plugins.BEMDataCollection.isValidLocationSettings, - $scope.recomputeLocStatus, - false, - ); + let checkSettings = function() { + console.log("Refresh location settings"); + return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationSettings, + $scope.recomputeLocStatus, false); }; - let fixPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.fixLocationPermissions, - $scope.recomputeLocStatus, - true, - ).then((error) => (locPermissionsCheck.desc = error)); + let fixPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationPermissions, + $scope.recomputeLocStatus, true).then((error) => locPermissionsCheck.desc = error); }; - let checkPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.isValidLocationPermissions, - $scope.recomputeLocStatus, - false, - ); + let checkPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationPermissions, + $scope.recomputeLocStatus, false); }; - var androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-gte-9'; + var androidSettingsDescTag = "intro.appstatus.locsettings.description.android-gte-9"; if (version < 9) { - androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-lt-9'; + androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; } - var androidPermDescTag = 'intro.appstatus.locperms.description.android-gte-12'; - if ($scope.osver < 6) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; + var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; + if($scope.osver < 6) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; } else if ($scope.osver < 10) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-6-9'; + androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; } else if ($scope.osver < 11) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-10'; + androidPermDescTag= "intro.appstatus.locperms.description.android-10"; } else if ($scope.osver < 12) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-11'; + androidPermDescTag= "intro.appstatus.locperms.description.android-11"; } - console.log('description tags are ' + androidSettingsDescTag + ' ' + androidPermDescTag); + console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag); // location settings let locSettingsCheck = { - name: i18next.t('intro.appstatus.locsettings.name'), - desc: i18next.t(androidSettingsDescTag), - statusState: false, - fix: fixSettings, - refresh: checkSettings, - }; + name: i18next.t("intro.appstatus.locsettings.name"), + desc: i18next.t(androidSettingsDescTag), + statusState: false, + fix: fixSettings, + refresh: checkSettings + } let locPermissionsCheck = { - name: i18next.t('intro.appstatus.locperms.name'), - desc: i18next.t(androidPermDescTag), - statusState: false, - fix: fixPerms, - refresh: checkPerms, - }; + name: i18next.t("intro.appstatus.locperms.name"), + desc: i18next.t(androidPermDescTag), + statusState: false, + fix: fixPerms, + refresh: checkPerms + } $scope.locChecks = [locSettingsCheck, locPermissionsCheck]; refreshChecks($scope.locChecks, $scope.recomputeLocStatus); - }; + } - $scope.setupIOSLocChecks = function (platform, version) { - let fixSettings = function () { - console.log('Fix and refresh location settings'); - return checkOrFix( - locSettingsCheck, - $window.cordova.plugins.BEMDataCollection.fixLocationSettings, - $scope.recomputeLocStatus, - true, - ); + $scope.setupIOSLocChecks = function(platform, version) { + let fixSettings = function() { + console.log("Fix and refresh location settings"); + return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationSettings, + $scope.recomputeLocStatus, true); }; - let checkSettings = function () { - console.log('Refresh location settings'); - return checkOrFix( - locSettingsCheck, - $window.cordova.plugins.BEMDataCollection.isValidLocationSettings, - $scope.recomputeLocStatus, - false, - ); + let checkSettings = function() { + console.log("Refresh location settings"); + return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationSettings, + $scope.recomputeLocStatus, false); }; - let fixPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.fixLocationPermissions, - $scope.recomputeLocStatus, - true, - ).then((error) => (locPermissionsCheck.desc = error)); + let fixPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationPermissions, + $scope.recomputeLocStatus, true).then((error) => locPermissionsCheck.desc = error); }; - let checkPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.isValidLocationPermissions, - $scope.recomputeLocStatus, - false, - ); + let checkPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationPermissions, + $scope.recomputeLocStatus, false); }; - var iOSSettingsDescTag = 'intro.appstatus.locsettings.description.ios'; - var iOSPermDescTag = 'intro.appstatus.locperms.description.ios-gte-13'; - if ($scope.osver < 13) { - iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; + var iOSSettingsDescTag = "intro.appstatus.locsettings.description.ios"; + var iOSPermDescTag = "intro.appstatus.locperms.description.ios-gte-13"; + if($scope.osver < 13) { + iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; } - console.log('description tags are ' + iOSSettingsDescTag + ' ' + iOSPermDescTag); + console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag); // location settings let locSettingsCheck = { - name: i18next.t('intro.appstatus.locsettings.name'), - desc: i18next.t(iOSSettingsDescTag), - statusState: false, - fix: fixSettings, - refresh: checkSettings, - }; + name: i18next.t("intro.appstatus.locsettings.name"), + desc: i18next.t(iOSSettingsDescTag), + statusState: false, + fix: fixSettings, + refresh: checkSettings + } let locPermissionsCheck = { - name: i18next.t('intro.appstatus.locperms.name'), - desc: i18next.t(iOSPermDescTag), - statusState: false, - fix: fixPerms, - refresh: checkPerms, - }; + name: i18next.t("intro.appstatus.locperms.name"), + desc: i18next.t(iOSPermDescTag), + statusState: false, + fix: fixPerms, + refresh: checkPerms + } $scope.locChecks = [locSettingsCheck, locPermissionsCheck]; refreshChecks($scope.locChecks, $scope.recomputeLocStatus); - }; + } - $scope.setupAndroidFitnessChecks = function (platform, version) { - $scope.fitnessPermNeeded = $scope.osver >= 10; + $scope.setupAndroidFitnessChecks = function(platform, version) { + $scope.fitnessPermNeeded = ($scope.osver >= 10); - let fixPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.fixFitnessPermissions, - $scope.recomputeFitnessStatus, - true, - ).then((error) => (fitnessPermissionsCheck.desc = error)); + let fixPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixFitnessPermissions, + $scope.recomputeFitnessStatus, true).then((error) => fitnessPermissionsCheck.desc = error); }; - let checkPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.isValidFitnessPermissions, - $scope.recomputeFitnessStatus, - false, - ); + let checkPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidFitnessPermissions, + $scope.recomputeFitnessStatus, false); }; - + let fitnessPermissionsCheck = { - name: i18next.t('intro.appstatus.fitnessperms.name'), - desc: i18next.t('intro.appstatus.fitnessperms.description.android'), - fix: fixPerms, - refresh: checkPerms, - }; - $scope.overallFitnessName = i18next.t('intro.appstatus.overall-fitness-name-android'); + name: i18next.t("intro.appstatus.fitnessperms.name"), + desc: i18next.t("intro.appstatus.fitnessperms.description.android"), + fix: fixPerms, + refresh: checkPerms + } + $scope.overallFitnessName = i18next.t("intro.appstatus.overall-fitness-name-android"); $scope.fitnessChecks = [fitnessPermissionsCheck]; refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus); - }; + } - $scope.setupIOSFitnessChecks = function (platform, version) { + $scope.setupIOSFitnessChecks = function(platform, version) { $scope.fitnessPermNeeded = true; - let fixPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.fixFitnessPermissions, - $scope.recomputeFitnessStatus, - true, - ).then((error) => (fitnessPermissionsCheck.desc = error)); + let fixPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixFitnessPermissions, + $scope.recomputeFitnessStatus, true).then((error) => fitnessPermissionsCheck.desc = error); }; - let checkPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - $window.cordova.plugins.BEMDataCollection.isValidFitnessPermissions, - $scope.recomputeFitnessStatus, - false, - ); + let checkPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidFitnessPermissions, + $scope.recomputeFitnessStatus, false); }; - + let fitnessPermissionsCheck = { - name: i18next.t('intro.appstatus.fitnessperms.name'), - desc: i18next.t('intro.appstatus.fitnessperms.description.ios'), - fix: fixPerms, - refresh: checkPerms, - }; - $scope.overallFitnessName = i18next.t('intro.appstatus.overall-fitness-name-ios'); + name: i18next.t("intro.appstatus.fitnessperms.name"), + desc: i18next.t("intro.appstatus.fitnessperms.description.ios"), + fix: fixPerms, + refresh: checkPerms + } + $scope.overallFitnessName = i18next.t("intro.appstatus.overall-fitness-name-ios"); $scope.fitnessChecks = [fitnessPermissionsCheck]; refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus); - }; + } - $scope.setupAndroidNotificationChecks = function () { - let fixPerms = function () { - console.log('fix and refresh notification permissions'); - return checkOrFix( - appAndChannelNotificationsCheck, - $window.cordova.plugins.BEMDataCollection.fixShowNotifications, - $scope.recomputeNotificationStatus, - true, - ); + $scope.setupAndroidNotificationChecks = function() { + let fixPerms = function() { + console.log("fix and refresh notification permissions"); + return checkOrFix(appAndChannelNotificationsCheck, $window.cordova.plugins.BEMDataCollection.fixShowNotifications, + $scope.recomputeNotificationStatus, true); }; - let checkPerms = function () { - console.log('fix and refresh notification permissions'); - return checkOrFix( - appAndChannelNotificationsCheck, - $window.cordova.plugins.BEMDataCollection.isValidShowNotifications, - $scope.recomputeNotificationStatus, - false, - ); + let checkPerms = function() { + console.log("fix and refresh notification permissions"); + return checkOrFix(appAndChannelNotificationsCheck, $window.cordova.plugins.BEMDataCollection.isValidShowNotifications, + $scope.recomputeNotificationStatus, false); }; let appAndChannelNotificationsCheck = { - name: i18next.t('intro.appstatus.notificationperms.app-enabled-name'), - desc: i18next.t('intro.appstatus.notificationperms.description.android-enable'), - fix: fixPerms, - refresh: checkPerms, - }; + name: i18next.t("intro.appstatus.notificationperms.app-enabled-name"), + desc: i18next.t("intro.appstatus.notificationperms.description.android-enable"), + fix: fixPerms, + refresh: checkPerms + } $scope.notificationChecks = [appAndChannelNotificationsCheck]; refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus); - }; + } - $scope.setupAndroidBackgroundRestrictionChecks = function () { - let fixPerms = function () { - console.log('fix and refresh backgroundRestriction permissions'); - return checkOrFix( - unusedAppsUnrestrictedCheck, - $window.cordova.plugins.BEMDataCollection.fixUnusedAppRestrictions, - $scope.recomputeBackgroundRestrictionStatus, - true, - ); + $scope.setupAndroidBackgroundRestrictionChecks = function() { + let fixPerms = function() { + console.log("fix and refresh backgroundRestriction permissions"); + return checkOrFix(unusedAppsUnrestrictedCheck, $window.cordova.plugins.BEMDataCollection.fixUnusedAppRestrictions, + $scope.recomputeBackgroundRestrictionStatus, true); }; - let checkPerms = function () { - console.log('fix and refresh backgroundRestriction permissions'); - return checkOrFix( - unusedAppsUnrestrictedCheck, - $window.cordova.plugins.BEMDataCollection.isUnusedAppUnrestricted, - $scope.recomputeBackgroundRestrictionStatus, - false, - ); + let checkPerms = function() { + console.log("fix and refresh backgroundRestriction permissions"); + return checkOrFix(unusedAppsUnrestrictedCheck, $window.cordova.plugins.BEMDataCollection.isUnusedAppUnrestricted, + $scope.recomputeBackgroundRestrictionStatus, false); }; - let fixBatteryOpt = function () { - console.log('fix and refresh battery optimization permissions'); - return checkOrFix( - ignoreBatteryOptCheck, - $window.cordova.plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, - $scope.recomputeBackgroundRestrictionStatus, - true, - ); + let fixBatteryOpt = function() { + console.log("fix and refresh battery optimization permissions"); + return checkOrFix(ignoreBatteryOptCheck, $window.cordova.plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, + $scope.recomputeBackgroundRestrictionStatus, true); }; - let checkBatteryOpt = function () { - console.log('fix and refresh battery optimization permissions'); - return checkOrFix( - ignoreBatteryOptCheck, - $window.cordova.plugins.BEMDataCollection.isIgnoreBatteryOptimizations, - $scope.recomputeBackgroundRestrictionStatus, - false, - ); + let checkBatteryOpt = function() { + console.log("fix and refresh battery optimization permissions"); + return checkOrFix(ignoreBatteryOptCheck, $window.cordova.plugins.BEMDataCollection.isIgnoreBatteryOptimizations, + $scope.recomputeBackgroundRestrictionStatus, false); }; - var androidUnusedDescTag = - 'intro.appstatus.unusedapprestrict.description.android-disable-gte-13'; + var androidUnusedDescTag = "intro.appstatus.unusedapprestrict.description.android-disable-gte-13"; if ($scope.osver == 12) { - androidUnusedDescTag = 'intro.appstatus.unusedapprestrict.description.android-disable-12'; - } else if ($scope.osver < 12) { - androidUnusedDescTag = - 'intro.appstatus.unusedapprestrict.description.android-disable-lt-12'; + androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-12"; + } + else if ($scope.osver < 12) { + androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12"; } let unusedAppsUnrestrictedCheck = { - name: i18next.t('intro.appstatus.unusedapprestrict.name'), - desc: i18next.t(androidUnusedDescTag), - fix: fixPerms, - refresh: checkPerms, - }; + name: i18next.t("intro.appstatus.unusedapprestrict.name"), + desc: i18next.t(androidUnusedDescTag), + fix: fixPerms, + refresh: checkPerms + } let ignoreBatteryOptCheck = { - name: i18next.t('intro.appstatus.ignorebatteryopt.name'), - desc: i18next.t('intro.appstatus.ignorebatteryopt.description.android-disable'), - fix: fixBatteryOpt, - refresh: checkBatteryOpt, - }; + name: i18next.t("intro.appstatus.ignorebatteryopt.name"), + desc: i18next.t("intro.appstatus.ignorebatteryopt.description.android-disable"), + fix: fixBatteryOpt, + refresh: checkBatteryOpt + } $scope.backgroundRestrictionChecks = [unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck]; - refreshChecks( - $scope.backgroundRestrictionChecks, - $scope.recomputeBackgroundRestrictionStatus, - ); - }; + refreshChecks($scope.backgroundRestrictionChecks, $scope.recomputeBackgroundRestrictionStatus); + } - $scope.setupPermissionText = function () { - if ($scope.platform.toLowerCase() == 'ios') { - if ($scope.osver < 13) { - $scope.locationPermExplanation = i18next.t( - 'intro.permissions.locationPermExplanation-ios-lt-13', - ); + $scope.setupPermissionText = function() { + if($scope.platform.toLowerCase() == "ios") { + if($scope.osver < 13) { + $scope.locationPermExplanation = i18next.t("intro.permissions.locationPermExplanation-ios-lt-13"); } else { - $scope.locationPermExplanation = i18next.t( - 'intro.permissions.locationPermExplanation-ios-gte-13', - ); + $scope.locationPermExplanation = i18next.t("intro.permissions.locationPermExplanation-ios-gte-13"); } } - + $scope.backgroundRestricted = false; - if ($window.device.manufacturer.toLowerCase() == 'samsung') { + if($window.device.manufacturer.toLowerCase() == "samsung") { $scope.backgroundRestricted = true; - $scope.allowBackgroundInstructions = i18next.t('intro.allow_background.samsung'); + $scope.allowBackgroundInstructions = i18next.t("intro.allow_background.samsung"); } + + console.log("Explanation = "+$scope.locationPermExplanation); + } - console.log('Explanation = ' + $scope.locationPermExplanation); - }; - - $scope.checkLocationServicesEnabled = function () { - console.log('About to see if location services are enabled'); - }; - $ionicPlatform.ready().then(function () { - console.log('app is launched, should refresh'); + $scope.checkLocationServicesEnabled = function() { + console.log("About to see if location services are enabled"); + } + $ionicPlatform.ready().then(function() { + console.log("app is launched, should refresh"); $scope.platform = $window.device.platform; - $scope.osver = $window.device.version.split('.')[0]; + $scope.osver = $window.device.version.split(".")[0]; $scope.setupPermissionText(); $scope.setupLocChecks($scope.platform, $scope.osver); $scope.setupFitnessChecks($scope.platform, $scope.osver); $scope.setupNotificationChecks($scope.platform, $scope.osver); $scope.setupBackgroundRestrictionChecks($scope.platform, $scope.osver); - }); + }); - $ionicPlatform.on('resume', function () { - console.log('PERMISSION CHECK: app has resumed, should refresh'); + $ionicPlatform.on("resume", function() { + console.log("PERMISSION CHECK: app has resumed, should refresh"); refreshChecks($scope.locChecks, $scope.recomputeLocStatus); refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus); refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus); - refreshChecks( - $scope.backgroundRestrictionChecks, - $scope.recomputeBackgroundRestrictionStatus, - ); - }); + refreshChecks($scope.backgroundRestrictionChecks, $scope.recomputeBackgroundRestrictionStatus); + }); - $scope.$on('recomputeAppStatus', function (e, callback) { - console.log('PERMISSION CHECK: recomputing state'); + $scope.$on("recomputeAppStatus", function(e, callback) { + console.log("PERMISSION CHECK: recomputing state"); Promise.all([ - refreshChecks($scope.locChecks, $scope.recomputeLocStatus), - refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus), - refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus), - refreshChecks( - $scope.backgroundRestrictionChecks, - $scope.recomputeBackgroundRestrictionStatus, - ), - ]).then(() => { - callback($scope.overallstatus); - }); - }); - }, - ); + refreshChecks($scope.locChecks, $scope.recomputeLocStatus), + refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus), + refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus), + refreshChecks($scope.backgroundRestrictionChecks, $scope.recomputeBackgroundRestrictionStatus) + ]).then( () => { + callback($scope.overallstatus) + } + ); + }); +}); diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index 4103eabd0..074093999 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -1,4 +1,4 @@ -import { logDebug } from './plugin/logger'; +import { logDebug } from "./plugin/logger"; /** * @param url URL endpoint for the request diff --git a/www/js/components/ActionMenu.tsx b/www/js/components/ActionMenu.tsx index 0693acc8b..296717a00 100644 --- a/www/js/components/ActionMenu.tsx +++ b/www/js/components/ActionMenu.tsx @@ -1,44 +1,41 @@ -import React from 'react'; -import { Modal } from 'react-native'; -import { Dialog, Button, useTheme } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; -import { settingStyles } from '../control/ProfileSettings'; +import React from "react"; +import { Modal } from "react-native"; +import { Dialog, Button, useTheme } from "react-native-paper"; +import { useTranslation } from "react-i18next"; +import { settingStyles } from "../control/ProfileSettings"; -const ActionMenu = ({ vis, setVis, title, actionSet, onAction, onExit }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); +const ActionMenu = ({vis, setVis, title, actionSet, onAction, onExit}) => { - return ( - setVis(false)} transparent={true}> - setVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {title} - - {actionSet?.map((e) => ( - - ))} - - - - - - - ); -}; + const { t } = useTranslation(); + const { colors } = useTheme(); -export default ActionMenu; + return ( + setVis(false)} + transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {title} + + {actionSet?.map((e) => + + )} + + + + + + + ) +} + +export default ActionMenu; \ No newline at end of file diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index ccf1a6f74..1e957923b 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,12 +1,13 @@ -import React from 'react'; -import Chart, { Props as ChartProps } from './Chart'; -import { useTheme } from 'react-native-paper'; -import { getGradient } from './charting'; +import React from "react"; +import Chart, { Props as ChartProps } from "./Chart"; +import { useTheme } from "react-native-paper"; +import { getGradient } from "./charting"; type Props = Omit & { - meter?: { high: number; middle: number; dash_key: string }; -}; + meter?: {high: number, middle: number, dash_key: string}, +} const BarChart = ({ meter, ...rest }: Props) => { + const { colors } = useTheme(); if (meter) { @@ -14,11 +15,13 @@ const BarChart = ({ meter, ...rest }: Props) => { const darkenDegree = colorFor == 'border' ? 0.25 : 0; const alpha = colorFor == 'border' ? 1 : 0; return getGradient(chart, meter, dataset, ctx, alpha, darkenDegree); - }; + } rest.borderWidth = 3; } - return ; -}; + return ( + + ); +} export default BarChart; diff --git a/www/js/components/Carousel.tsx b/www/js/components/Carousel.tsx index 92febb32b..28a31ff6a 100644 --- a/www/js/components/Carousel.tsx +++ b/www/js/components/Carousel.tsx @@ -2,27 +2,26 @@ import React from 'react'; import { ScrollView, View } from 'react-native'; type Props = { - children: React.ReactNode; - cardWidth: number; - cardMargin: number; -}; + children: React.ReactNode, + cardWidth: number, + cardMargin: number, +} const Carousel = ({ children, cardWidth, cardMargin }: Props) => { const numCards = React.Children.count(children); return ( - + contentContainerStyle={{alignItems: 'flex-start'}}> {React.Children.map(children, (child, i) => ( - + {child} ))} - ); + ) }; export const s = { @@ -32,8 +31,8 @@ export const s = { paddingVertical: 10, }), carouselCard: (cardWidth, cardMargin, isFirst, isLast) => ({ - marginLeft: isFirst ? cardMargin : cardMargin / 2, - marginRight: isLast ? cardMargin : cardMargin / 2, + marginLeft: isFirst ? cardMargin : cardMargin/2, + marginRight: isLast ? cardMargin : cardMargin/2, width: cardWidth, scrollSnapAlign: 'center', scrollSnapStop: 'always', diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index d7687e424..79c6e40e4 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -1,3 +1,4 @@ + import React, { useEffect, useRef, useState, useMemo } from 'react'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; @@ -8,62 +9,48 @@ import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; ChartJS.register(...registerables, Annotation); -type XYPair = { x: number | string; y: number | string }; +type XYPair = { x: number|string, y: number|string }; type ChartDataset = { - label: string; - data: XYPair[]; + label: string, + data: XYPair[], }; export type Props = { - records: { label: string; x: number | string; y: number | string }[]; - axisTitle: string; - type: 'bar' | 'line'; - getColorForLabel?: (label: string) => string; - getColorForChartEl?: ( - chart, - currDataset: ChartDataset, - ctx: ScriptableContext<'bar' | 'line'>, - colorFor: 'background' | 'border', - ) => string | CanvasGradient | null; - borderWidth?: number; - lineAnnotations?: { value: number; label?: string; color?: string; position?: LabelPosition }[]; - isHorizontal?: boolean; - timeAxis?: boolean; - stacked?: boolean; -}; -const Chart = ({ - records, - axisTitle, - type, - getColorForLabel, - getColorForChartEl, - borderWidth, - lineAnnotations, - isHorizontal, - timeAxis, - stacked, -}: Props) => { + records: { label: string, x: number|string, y: number|string }[], + axisTitle: string, + type: 'bar'|'line', + getColorForLabel?: (label: string) => string, + getColorForChartEl?: (chart, currDataset: ChartDataset, ctx: ScriptableContext<'bar'|'line'>, colorFor: 'background'|'border') => string|CanvasGradient|null, + borderWidth?: number, + lineAnnotations?: { value: number, label?: string, color?:string, position?: LabelPosition }[], + isHorizontal?: boolean, + timeAxis?: boolean, + stacked?: boolean, +} +const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, borderWidth, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { + const { colors } = useTheme(); - const [numVisibleDatasets, setNumVisibleDatasets] = useState(1); + const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); const indexAxis = isHorizontal ? 'y' : 'x'; - const chartRef = useRef>(null); + const chartRef = useRef>(null); const [chartDatasets, setChartDatasets] = useState([]); - - const chartData = useMemo>(() => { + + const chartData = useMemo>(() => { let labelColorMap; // object mapping labels to colors if (getColorForLabel) { - const colorEntries = chartDatasets.map((d) => [d.label, getColorForLabel(d.label)]); + const colorEntries = chartDatasets.map(d => [d.label, getColorForLabel(d.label)] ); labelColorMap = dedupColors(colorEntries); } return { datasets: chartDatasets.map((e, i) => ({ ...e, - backgroundColor: (barCtx) => - labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background'), - borderColor: (barCtx) => - darkenOrLighten(labelColorMap?.[e.label], -0.5) || - getColorForChartEl(chartRef.current, e, barCtx, 'border'), + backgroundColor: (barCtx) => ( + labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background') + ), + borderColor: (barCtx) => ( + darkenOrLighten(labelColorMap?.[e.label], -.5) || getColorForChartEl(chartRef.current, e, barCtx, 'border') + ), borderWidth: borderWidth || 2, borderRadius: 3, })), @@ -73,16 +60,14 @@ const Chart = ({ // group records by label (this is the format that Chart.js expects) useEffect(() => { const d = records?.reduce((acc, record) => { - const existing = acc.find((e) => e.label == record.label); + const existing = acc.find(e => e.label == record.label); if (!existing) { acc.push({ label: record.label, - data: [ - { - x: record.x, - y: record.y, - }, - ], + data: [{ + x: record.x, + y: record.y, + }], }); } else { existing.data.push({ @@ -95,15 +80,11 @@ const Chart = ({ setChartDatasets(d); }, [records]); - const annotationsAtTop = - isHorizontal && lineAnnotations?.some((a) => !a.position || a.position == 'start'); + const annotationsAtTop = isHorizontal && lineAnnotations?.some(a => (!a.position || a.position == 'start')); return ( - - + { - setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()); - }, - ticks: timeAxis - ? {} - : { - callback: (value, i) => { - const label = chartDatasets[0].data[i].y; - if (typeof label == 'string' && label.includes('\n')) - return label.split('\n'); - return label; - }, - font: { size: 11 }, // default is 12, we want a tad smaller - }, - reverse: true, - stacked, + ...(isHorizontal ? { + y: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { + date: { zone: 'utc' }, + } : {}, + time: timeAxis ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } : {}, + beforeUpdate: (axis) => { + setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) + }, + ticks: timeAxis ? {} : { + callback: (value, i) => { + const label = chartDatasets[0].data[i].y; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; }, - x: { - title: { display: true, text: axisTitle }, - stacked, - }, - } - : { - x: { - offset: true, - type: timeAxis ? 'time' : 'category', - adapters: timeAxis - ? { - date: { zone: 'utc' }, - } - : {}, - time: timeAxis - ? { - unit: 'day', - tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - } - : {}, - ticks: timeAxis - ? {} - : { - callback: (value, i) => { - console.log('testing vertical', chartData, i); - const label = chartDatasets[0].data[i].x; - if (typeof label == 'string' && label.includes('\n')) - return label.split('\n'); - return label; - }, - }, - stacked, - }, - y: { - title: { display: true, text: axisTitle }, - stacked, + font: { size: 11 }, // default is 12, we want a tad smaller + }, + reverse: true, + stacked, + }, + x: { + title: { display: true, text: axisTitle }, + stacked, + }, + } : { + x: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { + date: { zone: 'utc' }, + } : {}, + time: timeAxis ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } : {}, + ticks: timeAxis ? {} : { + callback: (value, i) => { + console.log("testing vertical", chartData, i); + const label = chartDatasets[0].data[i].x; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; }, - }), + }, + stacked, + }, + y: { + title: { display: true, text: axisTitle }, + stacked, + }, + }), }, plugins: { ...(lineAnnotations?.length > 0 && { annotation: { clip: false, - annotations: lineAnnotations.map( - (a, i) => - ({ - type: 'line', - label: { - display: true, - padding: { x: 3, y: 1 }, - borderRadius: 0, - backgroundColor: 'rgba(0,0,0,.7)', - color: 'rgba(255,255,255,1)', - font: { size: 10 }, - position: a.position || 'start', - content: a.label, - yAdjust: annotationsAtTop ? -12 : 0, - }, - ...(isHorizontal - ? { xMin: a.value, xMax: a.value } - : { yMin: a.value, yMax: a.value }), - borderColor: a.color || colors.onBackground, - borderWidth: 3, - borderDash: [3, 3], - }) satisfies AnnotationOptions, - ), - }, + annotations: lineAnnotations.map((a, i) => ({ + type: 'line', + label: { + display: true, + padding: { x: 3, y: 1 }, + borderRadius: 0, + backgroundColor: 'rgba(0,0,0,.7)', + color: 'rgba(255,255,255,1)', + font: { size: 10 }, + position: a.position || 'start', + content: a.label, + yAdjust: annotationsAtTop ? -12 : 0, + }, + ...(isHorizontal ? { xMin: a.value, xMax: a.value } + : { yMin: a.value, yMax: a.value }), + borderColor: a.color || colors.onBackground, + borderWidth: 3, + borderDash: [3, 3], + } satisfies AnnotationOptions)), + } }), - }, + } }} // if there are annotations at the top of the chart, it overlaps with the legend // so we need to increase the spacing between the legend and the chart // https://stackoverflow.com/a/73498454 - plugins={ - annotationsAtTop && [ - { - id: 'increase-legend-spacing', - beforeInit(chart) { - const originalFit = (chart.legend as any).fit; - (chart.legend as any).fit = function fit() { - originalFit.bind(chart.legend)(); - this.height += 12; - }; - }, - }, - ] - } - /> + plugins={annotationsAtTop && [{ + id: "increase-legend-spacing", + beforeInit(chart) { + const originalFit = (chart.legend as any).fit; + (chart.legend as any).fit = function fit() { + originalFit.bind(chart.legend)(); + this.height += 12; + }; + } + }]} /> - ); -}; + ) +} export default Chart; diff --git a/www/js/components/DiaryButton.tsx b/www/js/components/DiaryButton.tsx index 6a04cb079..16c716f93 100644 --- a/www/js/components/DiaryButton.tsx +++ b/www/js/components/DiaryButton.tsx @@ -1,25 +1,28 @@ -import React from 'react'; +import React from "react"; import { StyleSheet } from 'react-native'; import { Button, ButtonProps, useTheme } from 'react-native-paper'; import color from 'color'; -import { Icon } from './Icon'; +import { Icon } from "./Icon"; + +type Props = ButtonProps & { fillColor?: string, borderColor?: string }; +const DiaryButton = ({ children, fillColor, borderColor, icon, ...rest } : Props) => { -type Props = ButtonProps & { fillColor?: string; borderColor?: string }; -const DiaryButton = ({ children, fillColor, borderColor, icon, ...rest }: Props) => { const { colors } = useTheme(); const textColor = rest.textColor || (fillColor ? colors.onPrimary : colors.primary); return ( - @@ -48,7 +51,7 @@ const s = StyleSheet.create({ icon: { marginRight: 4, verticalAlign: 'middle', - }, + } }); export default DiaryButton; diff --git a/www/js/components/Icon.tsx b/www/js/components/Icon.tsx index 3d13d0996..0b4c7253e 100644 --- a/www/js/components/Icon.tsx +++ b/www/js/components/Icon.tsx @@ -7,19 +7,14 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { IconButton } from 'react-native-paper'; -import { Props as IconButtonProps } from 'react-native-paper/lib/typescript/src/components/IconButton/IconButton'; +import { Props as IconButtonProps } from 'react-native-paper/lib/typescript/src/components/IconButton/IconButton' -export const Icon = ({ style, ...rest }: IconButtonProps) => { +export const Icon = ({style, ...rest}: IconButtonProps) => { return ( - + ); -}; +} const s = StyleSheet.create({ icon: { diff --git a/www/js/components/LeafletView.jsx b/www/js/components/LeafletView.jsx index 35d195a91..eb0c0bb78 100644 --- a/www/js/components/LeafletView.jsx +++ b/www/js/components/LeafletView.jsx @@ -1,15 +1,16 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { angularize } from '../angular-react-helper'; -import { object, string } from 'prop-types'; -import { View } from 'react-native'; -import { useTheme } from 'react-native-paper'; +import React, { useEffect, useRef, useState } from "react"; +import { angularize } from "../angular-react-helper"; +import { object, string } from "prop-types"; +import { View } from "react-native"; +import { useTheme } from "react-native-paper"; const mapSet = new Set(); export function invalidateMaps() { - mapSet.forEach((map) => map.invalidateSize()); + mapSet.forEach(map => map.invalidateSize()); } const LeafletView = ({ geojson, opts, ...otherProps }) => { + const mapElRef = useRef(null); const leafletMapRef = useRef(null); const geoJsonIdRef = useRef(null); @@ -23,7 +24,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { }).addTo(map); const gj = L.geoJson(geojson.data, { pointToLayer: pointToLayer, - style: (feature) => feature.style, + style: (feature) => feature.style }).addTo(map); const gjBounds = gj.getBounds().pad(0.2); map.fitBounds(gjBounds); @@ -46,7 +47,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { (happens because of FlashList's view recycling on the trip cards: https://shopify.github.io/flash-list/docs/recycling) */ if (geoJsonIdRef.current && geoJsonIdRef.current !== geojson.data.id) { - leafletMapRef.current.eachLayer((layer) => leafletMapRef.current.removeLayer(layer)); + leafletMapRef.current.eachLayer(layer => leafletMapRef.current.removeLayer(layer)); initMap(leafletMapRef.current); } @@ -54,7 +55,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { const mapElId = `map-${geojson.data.id.replace(/[^a-zA-Z0-9]/g, '')}`; return ( - + -
+
); }; -const startIcon = L.divIcon({ className: 'leaflet-div-icon-start', iconSize: [18, 18] }); -const stopIcon = L.divIcon({ className: 'leaflet-div-icon-stop', iconSize: [18, 18] }); +const startIcon = L.divIcon({className: 'leaflet-div-icon-start', iconSize: [18, 18]}); +const stopIcon = L.divIcon({className: 'leaflet-div-icon-stop', iconSize: [18, 18]}); -const pointToLayer = (feature, latlng) => { - switch (feature.properties.feature_type) { - case 'start_place': - return L.marker(latlng, { icon: startIcon }); - case 'end_place': - return L.marker(latlng, { icon: stopIcon }); + const pointToLayer = (feature, latlng) => { + switch(feature.properties.feature_type) { + case "start_place": return L.marker(latlng, {icon: startIcon}); + case "end_place": return L.marker(latlng, {icon: stopIcon}); // case "stop": return L.circleMarker(latlng); - default: - alert('Found unknown type in feature' + feature); - return L.marker(latlng); + default: alert("Found unknown type in feature" + feature); return L.marker(latlng) } }; LeafletView.propTypes = { geojson: object, - opts: object, -}; + opts: object +} angularize(LeafletView, 'LeafletView', 'emission.main.leaflet'); export default LeafletView; diff --git a/www/js/components/LineChart.tsx b/www/js/components/LineChart.tsx index 456642a63..66d21aac2 100644 --- a/www/js/components/LineChart.tsx +++ b/www/js/components/LineChart.tsx @@ -1,9 +1,11 @@ -import React from 'react'; -import Chart, { Props as ChartProps } from './Chart'; +import React from "react"; +import Chart, { Props as ChartProps } from "./Chart"; -type Props = Omit & {}; +type Props = Omit & { } const LineChart = ({ ...rest }: Props) => { - return ; -}; + return ( + + ); +} export default LineChart; diff --git a/www/js/components/NavBarButton.tsx b/www/js/components/NavBarButton.tsx index 294015152..7e9cb1217 100644 --- a/www/js/components/NavBarButton.tsx +++ b/www/js/components/NavBarButton.tsx @@ -1,39 +1,31 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import color from 'color'; -import { Button, useTheme } from 'react-native-paper'; -import { Icon } from './Icon'; +import React from "react"; +import { View, StyleSheet } from "react-native"; +import color from "color"; +import { Button, useTheme } from "react-native-paper"; +import { Icon } from "./Icon"; const NavBarButton = ({ children, icon, onPressAction, ...otherProps }) => { + const { colors } = useTheme(); - const buttonColor = color(colors.onBackground).alpha(0.07).rgb().string(); - const outlineColor = color(colors.onBackground).alpha(0.2).rgb().string(); + const buttonColor = color(colors.onBackground).alpha(.07).rgb().string(); + const outlineColor = color(colors.onBackground).alpha(.2).rgb().string(); - return ( - <> - - - ); + return (<> + + ); }; export const s = StyleSheet.create({ diff --git a/www/js/components/QrCode.jsx b/www/js/components/QrCode.jsx index 58dc780f5..0084b334f 100644 --- a/www/js/components/QrCode.jsx +++ b/www/js/components/QrCode.jsx @@ -2,10 +2,10 @@ Once the parent components, anyplace this is used, are converted to React, we can remove this wrapper and just use the QRCode component directly */ -import React from 'react'; -import { angularize } from '../angular-react-helper'; -import { string } from 'prop-types'; -import QRCode from 'react-qr-code'; +import React from "react"; +import { angularize } from "../angular-react-helper"; +import { string } from "prop-types"; +import QRCode from "react-qr-code"; const QrCode = ({ value }) => { return ; @@ -13,7 +13,7 @@ const QrCode = ({ value }) => { QrCode.propTypes = { value: string, -}; +} angularize(QrCode, 'QrCode', 'emission.main.qrcode'); export default QrCode; diff --git a/www/js/components/ToggleSwitch.tsx b/www/js/components/ToggleSwitch.tsx index 7f753a9a0..5fdf1cc46 100644 --- a/www/js/components/ToggleSwitch.tsx +++ b/www/js/components/ToggleSwitch.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import { SegmentedButtons, SegmentedButtonsProps, useTheme } from 'react-native-paper'; +import { SegmentedButtons, SegmentedButtonsProps, useTheme } from "react-native-paper"; + +const ToggleSwitch = ({ value, buttons, ...rest}: SegmentedButtonsProps) => { -const ToggleSwitch = ({ value, buttons, ...rest }: SegmentedButtonsProps) => { const { colors } = useTheme(); return ( - rest.onValueChange(v as any)} - buttons={buttons.map((o) => ({ + rest.onValueChange(v as any)} + buttons={buttons.map(o => ({ value: o.value, icon: o.icon, uncheckedColor: colors.onSurfaceDisabled, @@ -19,11 +18,9 @@ const ToggleSwitch = ({ value, buttons, ...rest }: SegmentedButtonsProps) => { borderBottomWidth: rest.density == 'high' ? 0 : 1, backgroundColor: value == o.value ? colors.elevation.level1 : colors.surfaceDisabled, }, - ...o, - }))} - {...rest} - /> - ); -}; + ...o + }))} {...rest} /> + ) +} export default ToggleSwitch; diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index 77490f7ff..f0da14619 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -15,23 +15,15 @@ export const defaultPalette = [ '#80afad', // teal oklch(72% 0.05 192) ]; -export function getChartHeight( - chartDatasets, - numVisibleDatasets, - indexAxis, - isHorizontal, - stacked, -) { +export function getChartHeight(chartDatasets, numVisibleDatasets, indexAxis, isHorizontal, stacked) { /* when horizontal charts have more data, they should get taller so they don't look squished */ if (isHorizontal) { // 'ideal' chart height is based on the number of datasets and number of unique index values const uniqueIndexVals = []; - chartDatasets.forEach((e) => - e.data.forEach((r) => { - if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); - }), - ); + chartDatasets.forEach(e => e.data.forEach(r => { + if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); + })); const numIndexVals = uniqueIndexVals.length; const heightPerIndexVal = stacked ? 36 : numVisibleDatasets * 8; const idealChartHeight = heightPerIndexVal * numIndexVals; @@ -49,11 +41,11 @@ export function getChartHeight( function getBarHeight(stacks) { let totalHeight = 0; - console.log('ctx stacks', stacks.x); - for (let val in stacks.x) { - if (!val.startsWith('_')) { + console.log("ctx stacks", stacks.x); + for(let val in stacks.x) { + if(!val.startsWith('_')){ totalHeight += stacks.x[val]; - console.log('ctx added ', val); + console.log("ctx added ", val ); } } return totalHeight; @@ -62,34 +54,27 @@ function getBarHeight(stacks) { //fill pattern creation //https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns function createDiagonalPattern(color = 'black') { - let shape = document.createElement('canvas'); - shape.width = 10; - shape.height = 10; - let c = shape.getContext('2d'); - c.strokeStyle = color; - c.lineWidth = 2; - c.beginPath(); - c.moveTo(2, 0); - c.lineTo(10, 8); - c.stroke(); - c.beginPath(); - c.moveTo(0, 8); - c.lineTo(2, 10); - c.stroke(); - return c.createPattern(shape, 'repeat'); + let shape = document.createElement('canvas') + shape.width = 10 + shape.height = 10 + let c = shape.getContext('2d') + c.strokeStyle = color + c.lineWidth = 2 + c.beginPath() + c.moveTo(2, 0) + c.lineTo(10, 8) + c.stroke() + c.beginPath() + c.moveTo(0, 8) + c.lineTo(2, 10) + c.stroke() + return c.createPattern(shape, 'repeat') } -export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, darken = 0) { +export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, darken=0) { if (!barCtx || !currDataset) return; let bar_height = getBarHeight(barCtx.parsed._stacks); - console.debug( - 'bar height for', - barCtx.raw.y, - ' is ', - bar_height, - 'which in chart is', - currDataset, - ); + console.debug("bar height for", barCtx.raw.y, " is ", bar_height, "which in chart is", currDataset); let meteredColor; if (bar_height > meter.high) meteredColor = colors.danger; else if (bar_height > meter.middle) meteredColor = colors.warn; @@ -110,7 +95,7 @@ const meterColors = { // https://www.joshwcomeau.com/gradient-generator?colors=fcab00|ba0000&angle=90&colorMode=lab&precision=3&easingCurve=0.25|0.75|0.75|0.25 between: ['#fcab00', '#ef8215', '#db5e0c', '#ce3d03', '#b70100'], // yellow-orange-red above: '#440000', // dark red -}; +} export function getGradient(chart, meter, currDataset, barCtx, alpha = null, darken = 0) { const { ctx, chartArea, scales } = chart; @@ -119,26 +104,19 @@ export function getGradient(chart, meter, currDataset, barCtx, alpha = null, dar const total = getBarHeight(barCtx.parsed._stacks); alpha = alpha || (currDataset.label == meter.dash_key ? 0.2 : 1); if (total < meter.middle) { - const adjColor = - darken || alpha - ? color(meterColors.below).darken(darken).alpha(alpha).rgb().string() - : meterColors.below; + const adjColor = darken||alpha ? color(meterColors.below).darken(darken).alpha(alpha).rgb().string() : meterColors.below; return adjColor; } const scaleMaxX = scales.x._range.max; gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0); meterColors.between.forEach((clr, i) => { - const clrPosition = - ((i + 1) / meterColors.between.length) * (meter.high - meter.middle) + meter.middle; + const clrPosition = ((i + 1) / meterColors.between.length) * (meter.high - meter.middle) + meter.middle; const adjColor = darken || alpha ? color(clr).darken(darken).alpha(alpha).rgb().string() : clr; gradient.addColorStop(Math.min(clrPosition, scaleMaxX) / scaleMaxX, adjColor); }); if (scaleMaxX > meter.high + 20) { - const adjColor = - darken || alpha - ? color(meterColors.above).darken(0.2).alpha(alpha).rgb().string() - : meterColors.above; - gradient.addColorStop((meter.high + 20) / scaleMaxX, adjColor); + const adjColor = darken||alpha ? color(meterColors.above).darken(0.2).alpha(alpha).rgb().string() : meterColors.above; + gradient.addColorStop((meter.high+20) / scaleMaxX, adjColor); } return gradient; } @@ -151,9 +129,9 @@ export function getGradient(chart, meter, currDataset, barCtx, alpha = null, dar export function darkenOrLighten(baseColor: string, change: number) { if (!baseColor) return baseColor; let colorObj = color(baseColor); - if (change < 0) { + if(change < 0) { // darkening appears more drastic than lightening, so we will be less aggressive (scale change by .5) - return colorObj.darken(Math.abs(change * 0.5)).hex(); + return colorObj.darken(Math.abs(change * .5)).hex(); } else { return colorObj.lighten(Math.abs(change)).hex(); } @@ -172,7 +150,7 @@ export const dedupColors = (colors: string[][]) => { if (duplicates.length > 1) { // there are duplicates; calculate an evenly-spaced adjustment for each one duplicates.forEach(([k, c], i) => { - const change = -maxAdjustment + ((maxAdjustment * 2) / (duplicates.length - 1)) * i; + const change = -maxAdjustment + (maxAdjustment*2 / (duplicates.length - 1)) * i; dedupedColors[k] = darkenOrLighten(clr, change); }); } else if (!dedupedColors[key]) { @@ -180,4 +158,4 @@ export const dedupColors = (colors: string[][]) => { } } return dedupedColors; -}; +} diff --git a/www/js/config/dynamic_config.js b/www/js/config/dynamic_config.js index cae1bdd7f..cb84924e7 100644 --- a/www/js/config/dynamic_config.js +++ b/www/js/config/dynamic_config.js @@ -5,396 +5,344 @@ import { displayError, logDebug } from '../plugin/logger'; import i18next from 'i18next'; import { fetchUrlCached } from '../commHelper'; -angular - .module('emission.config.dynamic', ['emission.plugin.logger', 'emission.plugin.kvstore']) - .factory( - 'DynamicConfig', - function ($http, $ionicPlatform, $window, $state, $rootScope, $timeout, KVStore, Logger) { - // also used in the startprefs class - // but without importing this - const CONFIG_PHONE_UI = 'config/app_ui_config'; - const CONFIG_PHONE_UI_KVSTORE = 'CONFIG_PHONE_UI'; - const LOAD_TIMEOUT = 6000; // 6000 ms = 6 seconds +angular.module('emission.config.dynamic', ['emission.plugin.logger', + 'emission.plugin.kvstore']) +.factory('DynamicConfig', function($http, $ionicPlatform, + $window, $state, $rootScope, $timeout, KVStore, Logger) { + // also used in the startprefs class + // but without importing this + const CONFIG_PHONE_UI="config/app_ui_config"; + const CONFIG_PHONE_UI_KVSTORE ="CONFIG_PHONE_UI"; + const LOAD_TIMEOUT = 6000; // 6000 ms = 6 seconds - var dc = {}; - dc.UI_CONFIG_READY = 'UI_CONFIG_READY'; - dc.UI_CONFIG_CHANGED = 'UI_CONFIG_CHANGED'; - dc.isConfigReady = false; - dc.isConfigChanged = false; + var dc = {}; + dc.UI_CONFIG_READY="UI_CONFIG_READY"; + dc.UI_CONFIG_CHANGED="UI_CONFIG_CHANGED"; + dc.isConfigReady = false; + dc.isConfigChanged = false; - dc.configChanged = function () { + dc.configChanged = function() { if (dc.isConfigChanged) { - return Promise.resolve(dc.config); + return Promise.resolve(dc.config); } else { - return new Promise(function (resolve, reject) { - $rootScope.$on(dc.UI_CONFIG_CHANGED, (event, newConfig) => resolve(newConfig)); - }); + return new Promise(function(resolve, reject) { + $rootScope.$on(dc.UI_CONFIG_CHANGED, (event, newConfig) => resolve(newConfig)); + }); } - }; - dc.configReady = function () { + } + dc.configReady = function() { if (dc.isConfigReady) { - Logger.log('UI_CONFIG in configReady function, resolving immediately'); - return Promise.resolve(dc.config); + Logger.log("UI_CONFIG in configReady function, resolving immediately"); + return Promise.resolve(dc.config); } else { - Logger.log('UI_CONFIG in configReady function, about to create promise'); - return new Promise(function (resolve, reject) { - Logger.log( - 'Registering for UI_CONFIG_READY notification in dynamic_config inside the promise', - ); - $rootScope.$on(dc.UI_CONFIG_READY, (event, newConfig) => { - Logger.log( - 'Received UI_CONFIG_READY notification in dynamic_config, resolving promise', - ); - resolve(newConfig); + Logger.log("UI_CONFIG in configReady function, about to create promise"); + return new Promise(function(resolve, reject) { + Logger.log("Registering for UI_CONFIG_READY notification in dynamic_config inside the promise"); + $rootScope.$on(dc.UI_CONFIG_READY, (event, newConfig) => { + Logger.log("Received UI_CONFIG_READY notification in dynamic_config, resolving promise"); + resolve(newConfig) + }); }); - }); } - }; + } - /* Fetch and cache any surveys resources that are referenced by URL in the config, + /* Fetch and cache any surveys resources that are referenced by URL in the config, as well as the label_options config if it is present. This way they will be available when the user needs them, and we won't have to fetch them again unless local storage is cleared. */ - const cacheResourcesFromConfig = (config) => { - if (config.survey_info?.surveys) { - Object.values(config.survey_info.surveys).forEach((survey) => { - if (!survey?.formPath) throw new Error(i18next.t('config.survey-missing-formpath')); - fetchUrlCached(survey.formPath); - }); - } - if (config.label_options) { - fetchUrlCached(config.label_options); - } - }; + const cacheResourcesFromConfig = (config) => { + if (config.survey_info?.surveys) { + Object.values(config.survey_info.surveys).forEach((survey) => { + if (!survey?.formPath) + throw new Error(i18next.t('config.survey-missing-formpath')); + fetchUrlCached(survey.formPath); + }); + } + if (config.label_options) { + fetchUrlCached(config.label_options); + } + } - const readConfigFromServer = async (label) => { - const config = await fetchConfig(label); - Logger.log( - 'Successfully found config, result is ' + JSON.stringify(config).substring(0, 10), - ); + const readConfigFromServer = async (label) => { + const config = await fetchConfig(label); + Logger.log("Successfully found config, result is " + JSON.stringify(config).substring(0, 10)); - // fetch + cache any resources referenced in the config, but don't 'await' them so we don't block - // the config loading process - cacheResourcesFromConfig(config); + // fetch + cache any resources referenced in the config, but don't 'await' them so we don't block + // the config loading process + cacheResourcesFromConfig(config); - const connectionURL = config.server ? config.server.connectUrl : 'dev defaults'; - _fillStudyName(config); - _backwardsCompatSurveyFill(config); - Logger.log( - 'Successfully downloaded config with version ' + - config.version + - ' for ' + - config.intro.translated_text.en.deployment_name + - ' and data collection URL ' + - connectionURL, - ); - return config; - }; + const connectionURL = config.server ? config.server.connectUrl : "dev defaults"; + _fillStudyName(config); + _backwardsCompatSurveyFill(config); + Logger.log("Successfully downloaded config with version " + config.version + + " for " + config.intro.translated_text.en.deployment_name + + " and data collection URL " + connectionURL); + return config; + } - const fetchConfig = async (label, alreadyTriedLocal = false) => { - Logger.log('Received request to join ' + label); + const fetchConfig = async (label, alreadyTriedLocal=false) => { + Logger.log("Received request to join "+label); const downloadURL = `https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/${label}.nrel-op.json`; if (!__DEV__ || alreadyTriedLocal) { - Logger.log('Fetching config from github'); - const r = await fetch(downloadURL); - if (!r.ok) throw new Error('Unable to fetch config from github'); - return r.json(); - } else { - Logger.log('Running in dev environment, checking for locally hosted config'); - try { - const r = await fetch('http://localhost:9090/configs/' + label + '.nrel-op.json'); - if (!r.ok) throw new Error('Local config not found'); + Logger.log("Fetching config from github"); + const r = await fetch(downloadURL); + if (!r.ok) throw new Error('Unable to fetch config from github'); return r.json(); - } catch (err) { - Logger.log('Local config not found'); - return fetchConfig(label, true); - } } - }; + else { + Logger.log("Running in dev environment, checking for locally hosted config"); + try { + const r = await fetch('http://localhost:9090/configs/'+label+'.nrel-op.json'); + if (!r.ok) throw new Error('Local config not found'); + return r.json(); + } catch (err) { + Logger.log("Local config not found"); + return fetchConfig(label, true); + } + } + } - dc.loadSavedConfig = function () { + dc.loadSavedConfig = function() { const nativePlugin = $window.cordova.plugins.BEMUserCache; const rwDocRead = nativePlugin.getDocument(CONFIG_PHONE_UI, false); const kvDocRead = KVStore.get(CONFIG_PHONE_UI_KVSTORE); return Promise.all([rwDocRead, kvDocRead]) - .then(([rwConfig, kvStoreConfig]) => { - const savedConfig = kvStoreConfig ? kvStoreConfig : rwConfig; - Logger.log( - 'DYNAMIC CONFIG: kvStoreConfig key length = ' + - Object.keys(kvStoreConfig || {}).length + - ' rwConfig key length = ' + - Object.keys(rwConfig || {}).length + - ' using kvStoreConfig? ' + - (kvStoreConfig ? true : false), - ); - if (!kvStoreConfig && rwConfig) { - // Backwards compat, can remove at the end of 2023 - Logger.log( - 'DYNAMIC CONFIG: rwConfig found, kvStoreConfig not found, setting to fix backwards compat', - ); - KVStore.set(CONFIG_PHONE_UI_KVSTORE, rwConfig); - } - if ( - Object.keys(kvStoreConfig || {}).length > 0 && - Object.keys(rwConfig || {}).length == 0 - ) { - // Might as well sync the RW config if it doesn't exist and - // have triple-redundancy for this - nativePlugin.putRWDocument(CONFIG_PHONE_UI, kvStoreConfig); - } - Logger.log('DYNAMIC CONFIG: final selected config = ' + JSON.stringify(savedConfig)); - if (nativePlugin.isEmptyDoc(savedConfig)) { - Logger.log('Found empty saved ui config, returning null'); - return undefined; - } else { - Logger.log('Found previously stored ui config, returning it'); - _fillStudyName(savedConfig); - _backwardsCompatSurveyFill(savedConfig); - return savedConfig; - } - }) - .catch((err) => Logger.displayError(i18next.t('config.unable-read-saved-config'), err)); - }; + .then(([rwConfig, kvStoreConfig]) => { + const savedConfig = kvStoreConfig? kvStoreConfig : rwConfig; + Logger.log("DYNAMIC CONFIG: kvStoreConfig key length = "+ Object.keys(kvStoreConfig || {}).length + +" rwConfig key length = "+ Object.keys(rwConfig || {}).length + +" using kvStoreConfig? "+(kvStoreConfig? true: false)); + if (!kvStoreConfig && rwConfig) { + // Backwards compat, can remove at the end of 2023 + Logger.log("DYNAMIC CONFIG: rwConfig found, kvStoreConfig not found, setting to fix backwards compat"); + KVStore.set(CONFIG_PHONE_UI_KVSTORE, rwConfig); + } + if ((Object.keys(kvStoreConfig || {}).length > 0) + && (Object.keys(rwConfig || {}).length == 0)) { + // Might as well sync the RW config if it doesn't exist and + // have triple-redundancy for this + nativePlugin.putRWDocument(CONFIG_PHONE_UI, kvStoreConfig); + } + Logger.log("DYNAMIC CONFIG: final selected config = "+JSON.stringify(savedConfig)); + if (nativePlugin.isEmptyDoc(savedConfig)) { + Logger.log("Found empty saved ui config, returning null"); + return undefined; + } else { + Logger.log("Found previously stored ui config, returning it"); + _fillStudyName(savedConfig); + _backwardsCompatSurveyFill(savedConfig); + return savedConfig; + } + }) + .catch((err) => Logger.displayError(i18next.t('config.unable-read-saved-config'), err)); + } - dc.resetConfigAndRefresh = function () { - const resetNativePromise = $window.cordova.plugins.BEMUserCache.putRWDocument( - CONFIG_PHONE_UI, - {}, - ); + dc.resetConfigAndRefresh = function() { + const resetNativePromise = $window.cordova.plugins.BEMUserCache.putRWDocument(CONFIG_PHONE_UI, {}); const resetKVStorePromise = KVStore.set(CONFIG_PHONE_UI_KVSTORE, {}); - Promise.all([resetNativePromise, resetKVStorePromise]).then($window.location.reload(true)); - }; + Promise.all([resetNativePromise, resetKVStorePromise]) + .then($window.location.reload(true)); + } - /** - * loadNewConfig download and load a new config from the server if it is a differ - * @param {[]} urlComponents specify the label of the config to load - * @param {} thenGoToIntro whether to go to the intro screen after loading the config - * @param {} [existingVersion=null] if the new config's version is the same, we won't update - * @returns {boolean} boolean representing whether the config was updated or not - */ - var loadNewConfig = function (newStudyLabel, thenGoToIntro, existingVersion = null) { - return readConfigFromServer(newStudyLabel) - .then((downloadedConfig) => { + /** + * loadNewConfig download and load a new config from the server if it is a differ + * @param {[]} urlComponents specify the label of the config to load + * @param {} thenGoToIntro whether to go to the intro screen after loading the config + * @param {} [existingVersion=null] if the new config's version is the same, we won't update + * @returns {boolean} boolean representing whether the config was updated or not + */ + var loadNewConfig = function (newStudyLabel, thenGoToIntro, existingVersion=null) { + return readConfigFromServer(newStudyLabel).then((downloadedConfig) => { if (downloadedConfig.version == existingVersion) { - Logger.log('UI_CONFIG: Not updating config because version is the same'); - return Promise.resolve(false); + Logger.log("UI_CONFIG: Not updating config because version is the same"); + return Promise.resolve(false); } // we want to validate before saving because we don't want to save // an invalid configuration const subgroup = dc.extractSubgroup(dc.scannedToken, downloadedConfig); // we can use angular.extend since urlComponents is not nested // need to change this to angular.merge if that changes - const toSaveConfig = angular.extend(downloadedConfig, { - joined: { opcode: dc.scannedToken, study_name: newStudyLabel, subgroup: subgroup }, - }); + const toSaveConfig = angular.extend(downloadedConfig, + {joined: {opcode: dc.scannedToken, study_name: newStudyLabel, subgroup: subgroup}}); const storeConfigPromise = $window.cordova.plugins.BEMUserCache.putRWDocument( - CONFIG_PHONE_UI, - toSaveConfig, - ); + CONFIG_PHONE_UI, toSaveConfig); const storeInKVStorePromise = KVStore.set(CONFIG_PHONE_UI_KVSTORE, toSaveConfig); - const logSuccess = (storeResults) => - Logger.log( - 'UI_CONFIG: Stored dynamic config successfully, result = ' + - JSON.stringify(storeResults), - ); + const logSuccess = (storeResults) => Logger.log("UI_CONFIG: Stored dynamic config successfully, result = "+JSON.stringify(storeResults)); // loaded new config, so it is both ready and changed - return Promise.all([storeConfigPromise, storeInKVStorePromise]) - .then(([result, kvStoreResult]) => { + return Promise.all([storeConfigPromise, storeInKVStorePromise]).then( + ([result, kvStoreResult]) => { logSuccess(result); dc.saveAndNotifyConfigChanged(downloadedConfig); dc.saveAndNotifyConfigReady(downloadedConfig); - if (thenGoToIntro) $state.go('root.intro'); + if (thenGoToIntro) + $state.go("root.intro"); return true; - }) - .catch((storeError) => - Logger.displayError(i18next.t('config.unable-to-store-config'), storeError), - ); - }) - .catch((fetchErr) => { + }).catch((storeError) => + Logger.displayError(i18next.t('config.unable-to-store-config'), storeError)); + }).catch((fetchErr) => { displayError(fetchErr, i18next.t('config.unable-download-config')); - }); - }; + }); + } - dc.saveAndNotifyConfigReady = function (newConfig) { + dc.saveAndNotifyConfigReady = function(newConfig) { dc.config = newConfig; dc.isConfigReady = true; - console.log('Broadcasting event ' + dc.UI_CONFIG_READY); + console.log("Broadcasting event "+dc.UI_CONFIG_READY); $rootScope.$broadcast(dc.UI_CONFIG_READY, newConfig); - }; + } - dc.saveAndNotifyConfigChanged = function (newConfig) { + dc.saveAndNotifyConfigChanged = function(newConfig) { dc.config = newConfig; dc.isConfigChanged = true; - console.log('Broadcasting event ' + dc.UI_CONFIG_CHANGED); + console.log("Broadcasting event "+dc.UI_CONFIG_CHANGED); $rootScope.$broadcast(dc.UI_CONFIG_CHANGED, newConfig); - }; + } - const _getStudyName = function (connectUrl) { + const _getStudyName = function(connectUrl) { const orig_host = new URL(connectUrl).hostname; - const first_domain = orig_host.split('.')[0]; - if (first_domain == 'openpath-stage') { - return 'stage'; - } - const openpath_index = first_domain.search('-openpath'); - if (openpath_index == -1) { - return undefined; - } - const study_name = first_domain.substr(0, openpath_index); + const first_domain = orig_host.split(".")[0]; + if (first_domain == "openpath-stage") { return "stage"; } + const openpath_index = first_domain.search("-openpath"); + if (openpath_index == -1) { return undefined; } + const study_name = first_domain.substr(0,openpath_index); return study_name; - }; + } - const _fillStudyName = function (config) { + const _fillStudyName = function(config) { if (!config.name) { - if (config.server) { - config.name = _getStudyName(config.server.connectUrl); - } else { - config.name = 'dev'; - } + if (config.server) { + config.name = _getStudyName(config.server.connectUrl); + } else { + config.name = "dev"; + } } - }; + } - const _backwardsCompatSurveyFill = function (config) { + const _backwardsCompatSurveyFill = function(config) { if (!config.survey_info) { - config.survey_info = { - surveys: { - UserProfileSurvey: { - formPath: 'json/demo-survey-v2.json', - version: 1, - compatibleWith: 1, - dataKey: 'manual/demographic_survey', - labelTemplate: { - en: 'Answered', - es: 'Contestada', - }, + config.survey_info = { + "surveys": { + "UserProfileSurvey": { + "formPath": "json/demo-survey-v2.json", + "version": 1, + "compatibleWith": 1, + "dataKey": "manual/demographic_survey", + "labelTemplate": { + "en": "Answered", + "es": "Contestada" + } + } }, - }, - 'trip-labels': 'MULTILABEL', - }; + "trip-labels": "MULTILABEL" + } } - }; + } - /* - * We want to support both old style and new style tokens. - * Theoretically, we don't need anything from this except the study - * name, but we should re-validate the token for extra robustness. - * The control flow here is a bit tricky, though. - * - we need to first get the study name - * - then we need to retrieve the study config - * - then we need to re-validate the token against the study config, - * and the subgroups in the study config, in particular. - * - * So let's support two separate functions here - extractStudyName and extractSubgroup - */ - dc.extractStudyName = function (token) { - const tokenParts = token.split('_'); + /* + * We want to support both old style and new style tokens. + * Theoretically, we don't need anything from this except the study + * name, but we should re-validate the token for extra robustness. + * The control flow here is a bit tricky, though. + * - we need to first get the study name + * - then we need to retrieve the study config + * - then we need to re-validate the token against the study config, + * and the subgroups in the study config, in particular. + * + * So let's support two separate functions here - extractStudyName and extractSubgroup + */ + dc.extractStudyName = function(token) { + const tokenParts = token.split("_"); if (tokenParts.length < 3) { // all tokens must have at least nrelop_[study name]_... - throw new Error(i18next.t('config.not-enough-parts-old-style', { token: token })); + throw new Error(i18next.t('config.not-enough-parts-old-style', {"token": token})); } - if (tokenParts[0] != 'nrelop') { - throw new Error(i18next.t('config.no-nrelop-start', { token: token })); + if (tokenParts[0] != "nrelop") { + throw new Error(i18next.t('config.no-nrelop-start', {token: token})); } return tokenParts[1]; - }; + } - dc.extractSubgroup = function (token, config) { + dc.extractSubgroup = function(token, config) { if (config.opcode) { - // new style study, expects token with sub-group - const tokenParts = token.split('_'); - if (tokenParts.length <= 3) { - // no subpart defined - throw new Error(i18next.t('config.not-enough-parts', { token: token })); - } - if (config.opcode.subgroups) { - if (config.opcode.subgroups.indexOf(tokenParts[2]) == -1) { - // subpart not in config list - throw new Error( - i18next.t('config.invalid-subgroup', { - token: token, - subgroup: tokenParts[2], - config_subgroups: config.opcode.subgroups, - }), - ); - } else { - console.log( - 'subgroup ' + tokenParts[2] + ' found in list ' + config.opcode.subgroups, - ); - return tokenParts[2]; + // new style study, expects token with sub-group + const tokenParts = token.split("_"); + if (tokenParts.length <= 3) { // no subpart defined + throw new Error(i18next.t('config.not-enough-parts', {token: token})); } - } else { - if (tokenParts[2] != 'default') { - // subpart not in config list - throw new Error(i18next.t('config.invalid-subgroup-no-default', { token: token })); + if (config.opcode.subgroups) { + if (config.opcode.subgroups.indexOf(tokenParts[2]) == -1) { + // subpart not in config list + throw new Error(i18next.t('config.invalid-subgroup', {token: token, subgroup: tokenParts[2], config_subgroups: config.opcode.subgroups})); + } else { + console.log("subgroup "+tokenParts[2]+" found in list "+config.opcode.subgroups); + return tokenParts[2]; + } } else { - console.log("no subgroups in config, 'default' subgroup found in token "); - return tokenParts[2]; + if (tokenParts[2] != "default") { + // subpart not in config list + throw new Error(i18next.t('config.invalid-subgroup-no-default', {token: token})); + } else { + console.log("no subgroups in config, 'default' subgroup found in token "); + return tokenParts[2]; + } } - } } else { - /* old style study, expect token without subgroup - * nothing further to validate at this point - * only validation required is `nrelop_` and valid study name - * first is already handled in extractStudyName, second is handled - * by default since download will fail if it is invalid - */ - console.log('Old-style study, expecting token without a subgroup...'); - return undefined; + /* old style study, expect token without subgroup + * nothing further to validate at this point + * only validation required is `nrelop_` and valid study name + * first is already handled in extractStudyName, second is handled + * by default since download will fail if it is invalid + */ + console.log("Old-style study, expecting token without a subgroup..."); + return undefined; } - }; + } + - dc.initByUser = function (urlComponents) { + + dc.initByUser = function(urlComponents) { dc.scannedToken = urlComponents.token; try { - const newStudyLabel = dc.extractStudyName(dc.scannedToken); - return ( - loadNewConfig(newStudyLabel, true) - // on successful download, cache the token in the rootScope - .then((wasUpdated) => { - $rootScope.scannedToken = dc.scannedToken; - }) - .catch((fetchErr) => { - Logger.displayError(i18next.t('config.unable-download-config'), fetchErr); - }) - ); + const newStudyLabel = dc.extractStudyName(dc.scannedToken); + return loadNewConfig(newStudyLabel, true) + // on successful download, cache the token in the rootScope + .then((wasUpdated) => {$rootScope.scannedToken = dc.scannedToken}) + .catch((fetchErr) => { + Logger.displayError(i18next.t('config.unable-download-config'), fetchErr); + }); } catch (error) { - Logger.displayError(i18next.t('config.invalid-opcode-format'), error); - return Promise.reject(error); + Logger.displayError(i18next.t('config.invalid-opcode-format'), error); + return Promise.reject(error); } - }; - dc.initAtLaunch = function () { - dc.loadSavedConfig() - .then((existingConfig) => { + }; + dc.initAtLaunch = function () { + dc.loadSavedConfig().then((existingConfig) => { if (!existingConfig) { - return Logger.log('UI_CONFIG: No existing config, skipping'); + return Logger.log("UI_CONFIG: No existing config, skipping"); } // if 'autoRefresh' is set, we will check for updates if (existingConfig.autoRefresh) { - loadNewConfig(existingConfig.joined.study_name, false, existingConfig.version) - .then((wasUpdated) => { - if (!wasUpdated) { - // config was not updated so we will proceed with existing config - $rootScope.$evalAsync(() => dc.saveAndNotifyConfigReady(existingConfig)); - } - }) - .catch((fetchErr) => { - // if we can't check for an updated config, we will proceed with the existing config - Logger.log('UI_CONFIG: Unable to check for update, skipping', fetchErr); - $rootScope.$evalAsync(() => dc.saveAndNotifyConfigReady(existingConfig)); - }); + loadNewConfig(existingConfig.joined.study_name, false, existingConfig.version) + .then((wasUpdated) => { + if (!wasUpdated) { + // config was not updated so we will proceed with existing config + $rootScope.$evalAsync(() => dc.saveAndNotifyConfigReady(existingConfig)); + } + }).catch((fetchErr) => { + // if we can't check for an updated config, we will proceed with the existing config + Logger.log("UI_CONFIG: Unable to check for update, skipping", fetchErr); + $rootScope.$evalAsync(() => dc.saveAndNotifyConfigReady(existingConfig)); + }); } else { - Logger.log( - 'UI_CONFIG: autoRefresh is false, not checking for updates. Using existing config', - ); - $rootScope.$apply(() => dc.saveAndNotifyConfigReady(existingConfig)); + Logger.log("UI_CONFIG: autoRefresh is false, not checking for updates. Using existing config") + $rootScope.$apply(() => dc.saveAndNotifyConfigReady(existingConfig)); } - }) - .catch((err) => { - Logger.displayError(i18next.t('config.error-loading-config-app-start', err)); - }); - }; - $ionicPlatform.ready().then(function () { + }).catch((err) => { + Logger.displayError(i18next.t('config.error-loading-config-app-start', err)) + }); + }; + $ionicPlatform.ready().then(function() { dc.initAtLaunch(); - }); - return dc; - }, - ); + }); + return dc; +}); diff --git a/www/js/config/enketo-config.js b/www/js/config/enketo-config.js index 07ac599c2..00ea6f4be 100644 --- a/www/js/config/enketo-config.js +++ b/www/js/config/enketo-config.js @@ -1,10 +1,10 @@ // https://github.com/enketo/enketo-core#global-configuration const enketoConfig = { - swipePage: false /* Enketo's use of swipe gestures depends on jquery-touchswipe, + swipePage: false, /* Enketo's use of swipe gestures depends on jquery-touchswipe, which is a legacy package, and problematic to load in webpack. - Let's just turn it off. */, - experimentalOptimizations: {} /* We aren't using any experimental optimizations, - but it has to be defined to avoid errors */, -}; + Let's just turn it off. */ + experimentalOptimizations: {}, /* We aren't using any experimental optimizations, + but it has to be defined to avoid errors */ +} export default enketoConfig; diff --git a/www/js/config/imperial.js b/www/js/config/imperial.js index 790def25a..133626064 100644 --- a/www/js/config/imperial.js +++ b/www/js/config/imperial.js @@ -2,9 +2,8 @@ import angular from 'angular'; -angular - .module('emission.config.imperial', ['emission.plugin.logger']) - .factory('ImperialConfig', function ($rootScope, DynamicConfig, Logger, $ionicPlatform) { +angular.module('emission.config.imperial', ['emission.plugin.logger']) +.factory('ImperialConfig', function($rootScope, DynamicConfig, Logger, $ionicPlatform) { // change to true if we want to use imperial units such as miles // I didn't want to do this since the US is one of the only countries that // still uses imperial units, but it looks like this will primarily be @@ -12,50 +11,41 @@ angular const KM_TO_MILES = 0.621371; - var ic = {}; + var ic = {} - ic.getFormattedDistanceInKm = function (dist_in_meters) { + ic.getFormattedDistanceInKm = function(dist_in_meters) { if (dist_in_meters > 1000) { - return Number.parseFloat((dist_in_meters / 1000).toFixed(0)); + return Number.parseFloat((dist_in_meters/1000).toFixed(0)); } else { - return Number.parseFloat((dist_in_meters / 1000).toFixed(3)); + return Number.parseFloat((dist_in_meters/1000).toFixed(3)); } - }; + } - ic.getFormattedDistanceInMiles = function (dist_in_meters) { - return Number.parseFloat( - (KM_TO_MILES * ic.getFormattedDistanceInKm(dist_in_meters)).toFixed(1), - ); - }; + ic.getFormattedDistanceInMiles = function(dist_in_meters) { + return Number.parseFloat((KM_TO_MILES * ic.getFormattedDistanceInKm(dist_in_meters)).toFixed(1)); + } - ic.getKmph = function (metersPerSec) { - return (metersPerSec * 3.6).toFixed(2); + ic.getKmph = function(metersPerSec) { + return (metersPerSec * 3.6).toFixed(2); }; - ic.getMph = function (metersPerSecond) { - return (KM_TO_MILES * Number.parseFloat(ic.getKmph(metersPerSecond))).toFixed(2); + ic.getMph = function(metersPerSecond) { + return (KM_TO_MILES * Number.parseFloat(ic.getKmph(metersPerSecond))).toFixed(2); }; - ic.init = function () { - ic.getFormattedDistance = ic.useImperial - ? ic.getFormattedDistanceInMiles - : ic.getFormattedDistanceInKm; - ic.getFormattedSpeed = ic.useImperial ? ic.getMph : ic.getKmph; - ic.getDistanceSuffix = ic.useImperial ? 'mi' : 'km'; - ic.getSpeedSuffix = ic.useImperial ? 'mph' : 'kmph'; - }; - $ionicPlatform.ready().then(function () { - Logger.log('UI_CONFIG: about to call configReady function in imperial.js'); - DynamicConfig.configReady() - .then((newConfig) => { - Logger.log( - 'UI_CONFIG: Resolved configReady promise in imperial.js, setting display_config ' + - JSON.stringify(newConfig.display_config), - ); + ic.init = function() { + ic.getFormattedDistance = ic.useImperial? ic.getFormattedDistanceInMiles : ic.getFormattedDistanceInKm; + ic.getFormattedSpeed = ic.useImperial? ic.getMph : ic.getKmph; + ic.getDistanceSuffix = ic.useImperial? "mi" : "km"; + ic.getSpeedSuffix = ic.useImperial? "mph" : "kmph"; + } + $ionicPlatform.ready().then(function() { + Logger.log("UI_CONFIG: about to call configReady function in imperial.js"); + DynamicConfig.configReady().then((newConfig) => { + Logger.log("UI_CONFIG: Resolved configReady promise in imperial.js, setting display_config "+JSON.stringify(newConfig.display_config)); ic.useImperial = newConfig.display_config.use_imperial; ic.init(); - }) - .catch((err) => Logger.displayError('Error while handling config in imperial.js', err)); + }).catch((err) => Logger.displayError("Error while handling config in imperial.js", err)); }); return ic; - }); +}); diff --git a/www/js/config/server_conn.js b/www/js/config/server_conn.js index 08b1c1f90..4e460d070 100644 --- a/www/js/config/server_conn.js +++ b/www/js/config/server_conn.js @@ -2,37 +2,35 @@ import angular from 'angular'; -angular - .module('emission.config.server_conn', ['emission.plugin.logger', 'emission.config.dynamic']) - .factory('ServerConnConfig', function ($rootScope, DynamicConfig, $ionicPlatform) { - var scc = {}; +angular.module('emission.config.server_conn', + ['emission.plugin.logger', 'emission.config.dynamic']) +.factory('ServerConnConfig', function($rootScope, DynamicConfig, $ionicPlatform) { + var scc = {} - scc.init = function (connectionConfig) { - if (connectionConfig) { - Logger.log('connectionConfig = ' + JSON.stringify(connectionConfig)); - $rootScope.connectUrl = connectionConfig.connectUrl; - $rootScope.aggregateAuth = connectionConfig.aggregate_call_auth; - window.cordova.plugins.BEMConnectionSettings.setSettings(connectionConfig); - } else { - // not displaying the error here since we have a backup - Logger.log('connectionConfig not defined, reverting to defaults'); - window.cordova.plugins.BEMConnectionSettings.getDefaultSettings().then( - function (defaultConfig) { - Logger.log('defaultConfig = ' + JSON.stringify(defaultConfig)); - $rootScope.connectUrl = defaultConfig.connectUrl; - $rootScope.aggregateAuth = 'no_auth'; - window.cordova.plugins.BEMConnectionSettings.setSettings(defaultConfig); - }, - ); - } + scc.init = function(connectionConfig) { + if (connectionConfig) { + Logger.log("connectionConfig = "+JSON.stringify(connectionConfig)); + $rootScope.connectUrl = connectionConfig.connectUrl; + $rootScope.aggregateAuth = connectionConfig.aggregate_call_auth; + window.cordova.plugins.BEMConnectionSettings.setSettings(connectionConfig); + } else { + // not displaying the error here since we have a backup + Logger.log("connectionConfig not defined, reverting to defaults"); + window.cordova.plugins.BEMConnectionSettings.getDefaultSettings().then(function(defaultConfig) { + Logger.log("defaultConfig = "+JSON.stringify(defaultConfig)); + $rootScope.connectUrl = defaultConfig.connectUrl; + $rootScope.aggregateAuth = "no_auth"; + window.cordova.plugins.BEMConnectionSettings.setSettings(defaultConfig); + }); + }; }; - console.log('Registering for the UI_CONFIG_CHANGED notification in the server connection'); - $ionicPlatform.ready().then(function () { + console.log("Registering for the UI_CONFIG_CHANGED notification in the server connection"); + $ionicPlatform.ready().then(function() { DynamicConfig.configChanged().then((newConfig) => { - Logger.log('Resolved UI_CONFIG_CHANGED promise in server_conn.js, filling in URL'); + Logger.log("Resolved UI_CONFIG_CHANGED promise in server_conn.js, filling in URL"); scc.init(newConfig.server); }); }); return scc; - }); +}); diff --git a/www/js/config/useImperialConfig.ts b/www/js/config/useImperialConfig.ts index 32c707bc6..58af79551 100644 --- a/www/js/config/useImperialConfig.ts +++ b/www/js/config/useImperialConfig.ts @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; -import useAppConfig from '../useAppConfig'; -import i18next from 'i18next'; +import React, { useEffect, useState } from "react"; +import useAppConfig from "../useAppConfig"; +import i18next from "i18next"; const KM_TO_MILES = 0.621371; const MPS_TO_KMPH = 3.6; @@ -15,21 +15,26 @@ const MPS_TO_KMPH = 3.6; e.g. "0.07 mi", "0.75 km" */ export const formatForDisplay = (value: number): string => { let opts: Intl.NumberFormatOptions = {}; - if (value >= 100) opts.maximumFractionDigits = 0; - else if (value >= 1) opts.maximumSignificantDigits = 3; - else opts.maximumFractionDigits = 2; + if (value >= 100) + opts.maximumFractionDigits = 0; + else if (value >= 1) + opts.maximumSignificantDigits = 3; + else + opts.maximumFractionDigits = 2; return Intl.NumberFormat(i18next.language, opts).format(value); -}; +} const convertDistance = (distMeters: number, imperial: boolean): number => { - if (imperial) return (distMeters / 1000) * KM_TO_MILES; + if (imperial) + return (distMeters / 1000) * KM_TO_MILES; return distMeters / 1000; -}; +} const convertSpeed = (speedMetersPerSec: number, imperial: boolean): number => { - if (imperial) return speedMetersPerSec * MPS_TO_KMPH * KM_TO_MILES; + if (imperial) + return speedMetersPerSec * MPS_TO_KMPH * KM_TO_MILES; return speedMetersPerSec * MPS_TO_KMPH; -}; +} export function useImperialConfig() { const { appConfig, loading } = useAppConfig(); @@ -41,13 +46,11 @@ export function useImperialConfig() { }, [appConfig, loading]); return { - distanceSuffix: useImperial ? 'mi' : 'km', - speedSuffix: useImperial ? 'mph' : 'kmph', - getFormattedDistance: useImperial - ? (d) => formatForDisplay(convertDistance(d, true)) - : (d) => formatForDisplay(convertDistance(d, false)), - getFormattedSpeed: useImperial - ? (s) => formatForDisplay(convertSpeed(s, true)) - : (s) => formatForDisplay(convertSpeed(s, false)), - }; + distanceSuffix: useImperial ? "mi" : "km", + speedSuffix: useImperial ? "mph" : "kmph", + getFormattedDistance: useImperial ? (d) => formatForDisplay(convertDistance(d, true)) + : (d) => formatForDisplay(convertDistance(d, false)), + getFormattedSpeed: useImperial ? (s) => formatForDisplay(convertSpeed(s, true)) + : (s) => formatForDisplay(convertSpeed(s, false)), + } } diff --git a/www/js/control/AlertBar.jsx b/www/js/control/AlertBar.jsx index c86401b03..fbac80056 100644 --- a/www/js/control/AlertBar.jsx +++ b/www/js/control/AlertBar.jsx @@ -1,37 +1,38 @@ -import React from 'react'; -import { Modal } from 'react-native'; +import React from "react"; +import { Modal } from "react-native"; import { Snackbar } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTranslation } from "react-i18next"; +import { SafeAreaView } from "react-native-safe-area-context"; -const AlertBar = ({ visible, setVisible, messageKey, messageAddition = undefined }) => { - const { t } = useTranslation(); - const onDismissSnackBar = () => setVisible(false); +const AlertBar = ({visible, setVisible, messageKey, messageAddition=undefined}) => { + const { t } = useTranslation(); + const onDismissSnackBar = () => setVisible(false); - let text = ''; - if (messageAddition) { - text = t(messageKey) + messageAddition; - } else { - text = t(messageKey); - } - - return ( - setVisible(false)} transparent={true}> - - { - onDismissSnackBar(); - }, + let text = ""; + if(messageAddition){ + text = t(messageKey) + messageAddition; + } + else { + text = t(messageKey); + } + + return ( + setVisible(false)} transparent={true}> + + { + onDismissSnackBar() + }, }}> - {text} - - + {text} + + - ); -}; - -export default AlertBar; + ); + }; + +export default AlertBar; \ No newline at end of file diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index 6364c2bf5..34f5820bf 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -1,525 +1,445 @@ //component to view and manage permission settings -import React, { useState, useEffect, useMemo, useRef } from 'react'; -import { Modal, useWindowDimensions, ScrollView } from 'react-native'; +import React, { useState, useEffect, useMemo, useRef } from "react"; +import { Modal, useWindowDimensions, ScrollView } from "react-native"; import { Dialog, Button, Text, useTheme } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; -import PermissionItem from '../appstatus/PermissionItem'; -import useAppConfig from '../useAppConfig'; -import useAppStateChange from '../useAppStateChange'; -import ExplainPermissions from '../appstatus/ExplainPermissions'; -import AlertBar from './AlertBar'; -import { settingStyles } from './ProfileSettings'; - -const AppStatusModal = ({ permitVis, setPermitVis }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const { appConfig, loading } = useAppConfig(); - - const { height: windowHeight } = useWindowDimensions(); - const osver = useRef(0); - const platform = useRef(''); - - const [error, setError] = useState(''); - const [errorVis, setErrorVis] = useState(false); - - const [explainVis, setExplainVis] = useState(false); - - const [checkList, setCheckList] = useState([]); - const [explanationList, setExplanationList] = useState>([]); - const [haveSetText, setHaveSetText] = useState(false); - - let iconMap = (statusState) => (statusState ? 'check-circle-outline' : 'alpha-x-circle-outline'); - let colorMap = (statusState) => (statusState ? colors.success : colors.danger); - - const overallStatus = useMemo(() => { - let status = true; - if (!checkList?.length) return undefined; // if checks not loaded yet, status is undetermined - checkList.forEach((lc) => { - console.debug('check in permission status for ' + lc.name + ':', lc.statusState); - if (lc.statusState === false) { - status = false; - } - }); - return status; - }, [checkList]); - - //using this function to update checks rather than mutate - //this cues React to update UI - function updateCheck(newObject) { - var tempList = [...checkList]; //make a copy rather than mutate - tempList.forEach((item, i) => { - if (item.name == newObject.name) { - tempList[i] = newObject; - } - }); - setCheckList(tempList); - } - - async function checkOrFix(checkObj, nativeFn, showError = true) { - console.log('checking object', checkObj.name, checkObj); - let newCheck = checkObj; - return nativeFn() - .then((status) => { - console.log('availability ', status); - newCheck.statusState = true; - updateCheck(newCheck); - console.log('after checking object', checkObj.name, checkList); +import { useTranslation } from "react-i18next"; +import PermissionItem from "../appstatus/PermissionItem"; +import useAppConfig from "../useAppConfig"; +import useAppStateChange from "../useAppStateChange"; +import ExplainPermissions from "../appstatus/ExplainPermissions"; +import AlertBar from "./AlertBar"; +import { settingStyles } from "./ProfileSettings"; + +const AppStatusModal = ({permitVis, setPermitVis}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const { appConfig, loading } = useAppConfig(); + + const { height: windowHeight } = useWindowDimensions(); + const osver = useRef(0); + const platform = useRef(""); + + const [error, setError] = useState(""); + const [errorVis, setErrorVis] = useState(false); + + const [explainVis, setExplainVis] = useState(false); + + const [checkList, setCheckList] = useState([]); + const [explanationList, setExplanationList] = useState>([]); + const [haveSetText, setHaveSetText] = useState(false); + + let iconMap = (statusState) => statusState ? "check-circle-outline" : "alpha-x-circle-outline"; + let colorMap = (statusState) => statusState ? colors.success : colors.danger; + + const overallStatus = useMemo(() => { + let status = true; + if (!checkList?.length) return undefined; // if checks not loaded yet, status is undetermined + checkList.forEach((lc) => { + console.debug('check in permission status for ' + lc.name + ':', lc.statusState); + if (lc.statusState === false) { + status = false; + } + }) return status; - }) - .catch((error) => { - console.log('Error', error); - if (showError) { - console.log('please fix again'); - setError(error); - setErrorVis(true); + }, [checkList]) + + //using this function to update checks rather than mutate + //this cues React to update UI + function updateCheck(newObject) { + var tempList = [...checkList]; //make a copy rather than mutate + tempList.forEach((item, i) => { + if(item.name == newObject.name){ + tempList[i] = newObject; + } + }); + setCheckList(tempList); + } + + async function checkOrFix(checkObj, nativeFn, showError=true) { + console.log("checking object", checkObj.name, checkObj); + let newCheck = checkObj; + return nativeFn() + .then((status) => { + console.log("availability ", status) + newCheck.statusState = true; + updateCheck(newCheck); + console.log("after checking object", checkObj.name, checkList); + return status; + }).catch((error) => { + console.log("Error", error) + if (showError) { + console.log("please fix again"); + setError(error); + setErrorVis(true); + }; + newCheck.statusState = false; + updateCheck(newCheck); + console.log("after checking object", checkObj.name, checkList); + return error; + }); + } + + function setupAndroidLocChecks() { + let fixSettings = function() { + console.log("Fix and refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationSettings, true); + }; + let checkSettings = function() { + console.log("Refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, false); + }; + let fixPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, + true).then((error) => {if(error){locPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, false); + }; + var androidSettingsDescTag = "intro.appstatus.locsettings.description.android-gte-9"; + if (osver.current < 9) { + androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; } - newCheck.statusState = false; - updateCheck(newCheck); - console.log('after checking object', checkObj.name, checkList); - return error; - }); - } - - function setupAndroidLocChecks() { - let fixSettings = function () { - console.log('Fix and refresh location settings'); - return checkOrFix( - locSettingsCheck, - window['cordova'].plugins.BEMDataCollection.fixLocationSettings, - true, - ); - }; - let checkSettings = function () { - console.log('Refresh location settings'); - return checkOrFix( - locSettingsCheck, - window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, - false, - ); - }; - let fixPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, - true, - ).then((error) => { - if (error) { - locPermissionsCheck.desc = error; + var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; + if(osver.current < 6) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; + } else if (osver.current < 10) { + androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; + } else if (osver.current < 11) { + androidPermDescTag= "intro.appstatus.locperms.description.android-10"; + } else if (osver.current < 12) { + androidPermDescTag= "intro.appstatus.locperms.description.android-11"; } - }); - }; - let checkPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, - false, - ); - }; - var androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-gte-9'; - if (osver.current < 9) { - androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-lt-9'; - } - var androidPermDescTag = 'intro.appstatus.locperms.description.android-gte-12'; - if (osver.current < 6) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; - } else if (osver.current < 10) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-6-9'; - } else if (osver.current < 11) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-10'; - } else if (osver.current < 12) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-11'; + console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag); + // location settings + let locSettingsCheck = { + name: t("intro.appstatus.locsettings.name"), + desc: t(androidSettingsDescTag), + fix: fixSettings, + refresh: checkSettings + } + let locPermissionsCheck = { + name: t("intro.appstatus.locperms.name"), + desc: t(androidPermDescTag), + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(locSettingsCheck, locPermissionsCheck); + setCheckList(tempChecks); } - console.log('description tags are ' + androidSettingsDescTag + ' ' + androidPermDescTag); - // location settings - let locSettingsCheck = { - name: t('intro.appstatus.locsettings.name'), - desc: t(androidSettingsDescTag), - fix: fixSettings, - refresh: checkSettings, - }; - let locPermissionsCheck = { - name: t('intro.appstatus.locperms.name'), - desc: t(androidPermDescTag), - fix: fixPerms, - refresh: checkPerms, - }; - let tempChecks = checkList; - tempChecks.push(locSettingsCheck, locPermissionsCheck); - setCheckList(tempChecks); - } - - function setupIOSLocChecks() { - let fixSettings = function () { - console.log('Fix and refresh location settings'); - return checkOrFix( - locSettingsCheck, - window['cordova'].plugins.BEMDataCollection.fixLocationSettings, - true, - ); - }; - let checkSettings = function () { - console.log('Refresh location settings'); - return checkOrFix( - locSettingsCheck, - window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, - false, - ); - }; - let fixPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, - true, - ).then((error) => { - if (error) { - locPermissionsCheck.desc = error; + + function setupIOSLocChecks() { + let fixSettings = function() { + console.log("Fix and refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationSettings, + true); + }; + let checkSettings = function() { + console.log("Refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, + false); + }; + let fixPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, + true).then((error) => {if(error){locPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, + false); + }; + var iOSSettingsDescTag = "intro.appstatus.locsettings.description.ios"; + var iOSPermDescTag = "intro.appstatus.locperms.description.ios-gte-13"; + if(osver.current < 13) { + iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; } - }); - }; - let checkPerms = function () { - console.log('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, - false, - ); - }; - var iOSSettingsDescTag = 'intro.appstatus.locsettings.description.ios'; - var iOSPermDescTag = 'intro.appstatus.locperms.description.ios-gte-13'; - if (osver.current < 13) { - iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; + console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag); + + const locSettingsCheck = { + name: t("intro.appstatus.locsettings.name"), + desc: t(iOSSettingsDescTag), + fix: fixSettings, + refresh: checkSettings + }; + const locPermissionsCheck = { + name: t("intro.appstatus.locperms.name"), + desc: t(iOSPermDescTag), + fix: fixPerms, + refresh: checkPerms + }; + let tempChecks = checkList; + tempChecks.push(locSettingsCheck, locPermissionsCheck); + setCheckList(tempChecks); } - console.log('description tags are ' + iOSSettingsDescTag + ' ' + iOSPermDescTag); - - const locSettingsCheck = { - name: t('intro.appstatus.locsettings.name'), - desc: t(iOSSettingsDescTag), - fix: fixSettings, - refresh: checkSettings, - }; - const locPermissionsCheck = { - name: t('intro.appstatus.locperms.name'), - desc: t(iOSPermDescTag), - fix: fixPerms, - refresh: checkPerms, - }; - let tempChecks = checkList; - tempChecks.push(locSettingsCheck, locPermissionsCheck); - setCheckList(tempChecks); - } - - function setupAndroidFitnessChecks() { - if (osver.current >= 10) { - let fixPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, - true, - ).then((error) => { - if (error) { - fitnessPermissionsCheck.desc = error; - } - }); - }; - let checkPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, - false, - ); - }; - - let fitnessPermissionsCheck = { - name: t('intro.appstatus.fitnessperms.name'), - desc: t('intro.appstatus.fitnessperms.description.android'), - fix: fixPerms, - refresh: checkPerms, - }; - let tempChecks = checkList; - tempChecks.push(fitnessPermissionsCheck); - setCheckList(tempChecks); + + function setupAndroidFitnessChecks() { + if(osver.current >= 10){ + let fixPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, + true).then((error) => {if(error){fitnessPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false); + }; + + let fitnessPermissionsCheck = { + name: t("intro.appstatus.fitnessperms.name"), + desc: t("intro.appstatus.fitnessperms.description.android"), + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(fitnessPermissionsCheck); + setCheckList(tempChecks); + } } - } - - function setupIOSFitnessChecks() { - let fixPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, - true, - ).then((error) => { - if (error) { - fitnessPermissionsCheck.desc = error; + + function setupIOSFitnessChecks() { + let fixPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, + true).then((error) => {if(error){fitnessPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false); + }; + + let fitnessPermissionsCheck = { + name: t("intro.appstatus.fitnessperms.name"), + desc: t("intro.appstatus.fitnessperms.description.ios"), + fix: fixPerms, + refresh: checkPerms } - }); - }; - let checkPerms = function () { - console.log('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, - false, - ); - }; - - let fitnessPermissionsCheck = { - name: t('intro.appstatus.fitnessperms.name'), - desc: t('intro.appstatus.fitnessperms.description.ios'), - fix: fixPerms, - refresh: checkPerms, - }; - let tempChecks = checkList; - tempChecks.push(fitnessPermissionsCheck); - setCheckList(tempChecks); - } - - function setupAndroidNotificationChecks() { - let fixPerms = function () { - console.log('fix and refresh notification permissions'); - return checkOrFix( - appAndChannelNotificationsCheck, - window['cordova'].plugins.BEMDataCollection.fixShowNotifications, - true, - ); - }; - let checkPerms = function () { - console.log('fix and refresh notification permissions'); - return checkOrFix( - appAndChannelNotificationsCheck, - window['cordova'].plugins.BEMDataCollection.isValidShowNotifications, - false, - ); - }; - let appAndChannelNotificationsCheck = { - name: t('intro.appstatus.notificationperms.app-enabled-name'), - desc: t('intro.appstatus.notificationperms.description.android-enable'), - fix: fixPerms, - refresh: checkPerms, - }; - let tempChecks = checkList; - tempChecks.push(appAndChannelNotificationsCheck); - setCheckList(tempChecks); - } - - function setupAndroidBackgroundRestrictionChecks() { - let fixPerms = function () { - console.log('fix and refresh backgroundRestriction permissions'); - return checkOrFix( - unusedAppsUnrestrictedCheck, - window['cordova'].plugins.BEMDataCollection.fixUnusedAppRestrictions, - true, - ); - }; - let checkPerms = function () { - console.log('fix and refresh backgroundRestriction permissions'); - return checkOrFix( - unusedAppsUnrestrictedCheck, - window['cordova'].plugins.BEMDataCollection.isUnusedAppUnrestricted, - false, - ); - }; - let fixBatteryOpt = function () { - console.log('fix and refresh battery optimization permissions'); - return checkOrFix( - ignoreBatteryOptCheck, - window['cordova'].plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, - true, - ); - }; - let checkBatteryOpt = function () { - console.log('fix and refresh battery optimization permissions'); - return checkOrFix( - ignoreBatteryOptCheck, - window['cordova'].plugins.BEMDataCollection.isIgnoreBatteryOptimizations, - false, - ); - }; - var androidUnusedDescTag = - 'intro.appstatus.unusedapprestrict.description.android-disable-gte-13'; - if (osver.current == 12) { - androidUnusedDescTag = 'intro.appstatus.unusedapprestrict.description.android-disable-12'; - } else if (osver.current < 12) { - androidUnusedDescTag = 'intro.appstatus.unusedapprestrict.description.android-disable-lt-12'; + let tempChecks = checkList; + tempChecks.push(fitnessPermissionsCheck); + setCheckList(tempChecks); } - let unusedAppsUnrestrictedCheck = { - name: t('intro.appstatus.unusedapprestrict.name'), - desc: t(androidUnusedDescTag), - fix: fixPerms, - refresh: checkPerms, - }; - let ignoreBatteryOptCheck = { - name: t('intro.appstatus.ignorebatteryopt.name'), - desc: t('intro.appstatus.ignorebatteryopt.description.android-disable'), - fix: fixBatteryOpt, - refresh: checkBatteryOpt, - }; - let tempChecks = checkList; - tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck); - setCheckList(tempChecks); - } - - function setupPermissionText() { - let tempExplanations = explanationList; - - let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); - let locExplanation = t('intro.appstatus.overall-loc-description'); - if (platform.current == 'ios') { - overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); - if (osver.current < 13) { - locExplanation = t('intro.permissions.locationPermExplanation-ios-lt-13'); - } else { - locExplanation = t('intro.permissions.locationPermExplanation-ios-gte-13'); - } + + function setupAndroidNotificationChecks() { + let fixPerms = function() { + console.log("fix and refresh notification permissions"); + return checkOrFix(appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.fixShowNotifications, + true); + }; + let checkPerms = function() { + console.log("fix and refresh notification permissions"); + return checkOrFix(appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.isValidShowNotifications, + false); + }; + let appAndChannelNotificationsCheck = { + name: t("intro.appstatus.notificationperms.app-enabled-name"), + desc: t("intro.appstatus.notificationperms.description.android-enable"), + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(appAndChannelNotificationsCheck); + setCheckList(tempChecks); } - tempExplanations.push({ name: t('intro.appstatus.overall-loc-name'), desc: locExplanation }); - tempExplanations.push({ - name: overallFitnessName, - desc: t('intro.appstatus.overall-fitness-description'), - }); - tempExplanations.push({ - name: t('intro.appstatus.overall-notification-name'), - desc: t('intro.appstatus.overall-notification-description'), - }); - tempExplanations.push({ - name: t('intro.appstatus.overall-background-restrictions-name'), - desc: t('intro.appstatus.overall-background-restrictions-description'), - }); - setExplanationList(tempExplanations); - - //TODO - update samsung handling based on feedback - - console.log('Explanation = ' + explanationList); - } - - function createChecklist() { - console.debug( - 'setting up checks, platform is ' + platform.current + 'and osver is ' + osver.current, - ); - if (platform.current == 'android') { - setupAndroidLocChecks(); - setupAndroidFitnessChecks(); - setupAndroidNotificationChecks(); - setupAndroidBackgroundRestrictionChecks(); - } else if (platform.current == 'ios') { - setupIOSLocChecks(); - setupIOSFitnessChecks(); - setupAndroidNotificationChecks(); - } else { - setError('Alert! unknownplatform, no tracking'); - setErrorVis(true); - console.log('Alert! unknownplatform, no tracking'); //need an alert, can use AlertBar? + function setupAndroidBackgroundRestrictionChecks() { + let fixPerms = function() { + console.log("fix and refresh backgroundRestriction permissions"); + return checkOrFix(unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.fixUnusedAppRestrictions, + true); + }; + let checkPerms = function() { + console.log("fix and refresh backgroundRestriction permissions"); + return checkOrFix(unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.isUnusedAppUnrestricted, + false); + }; + let fixBatteryOpt = function() { + console.log("fix and refresh battery optimization permissions"); + return checkOrFix(ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, + true); + }; + let checkBatteryOpt = function() { + console.log("fix and refresh battery optimization permissions"); + return checkOrFix(ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.isIgnoreBatteryOptimizations, + false); + }; + var androidUnusedDescTag = "intro.appstatus.unusedapprestrict.description.android-disable-gte-13"; + if (osver.current == 12) { + androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-12"; + } + else if (osver.current < 12) { + androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12"; + } + let unusedAppsUnrestrictedCheck = { + name: t("intro.appstatus.unusedapprestrict.name"), + desc: t(androidUnusedDescTag), + fix: fixPerms, + refresh: checkPerms + } + let ignoreBatteryOptCheck = { + name: t("intro.appstatus.ignorebatteryopt.name"), + desc: t("intro.appstatus.ignorebatteryopt.description.android-disable"), + fix: fixBatteryOpt, + refresh: checkBatteryOpt + } + let tempChecks = checkList; + tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck); + setCheckList(tempChecks); } - refreshAllChecks(); - } + function setupPermissionText() { + let tempExplanations = explanationList; + + let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); + let locExplanation = t('intro.appstatus.overall-loc-description'); + if(platform.current == "ios") { + overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); + if(osver.current < 13) { + locExplanation = (t("intro.permissions.locationPermExplanation-ios-lt-13")); + } else { + locExplanation = (t("intro.permissions.locationPermExplanation-ios-gte-13")); + } + } + tempExplanations.push({name: t('intro.appstatus.overall-loc-name'), desc: locExplanation}); + tempExplanations.push({name: overallFitnessName, desc: t('intro.appstatus.overall-fitness-description')}); + tempExplanations.push({name: t('intro.appstatus.overall-notification-name'), desc: t('intro.appstatus.overall-notification-description')}); + tempExplanations.push({name: t('intro.appstatus.overall-background-restrictions-name'), desc: t('intro.appstatus.overall-background-restrictions-description')}); + + setExplanationList(tempExplanations); + + //TODO - update samsung handling based on feedback - //refreshing checks with the plugins to update the check's statusState - function refreshAllChecks() { - //refresh each check - checkList.forEach((lc) => { - lc.refresh(); - }); - console.log('setting checks are', checkList); - } - - //recomputing checks updates the visual cues of their status - function recomputeAllChecks() { - console.log('recomputing checks', checkList); - checkList.forEach((lc) => { - lc.statusIcon = iconMap(lc.statusState); - lc.statusColor = colorMap(lc.statusState); - }); - } - - //anytime the status changes, may need to show modal - useEffect(() => { - let currentlyOpen = window?.appStatusModalOpened; - if (!currentlyOpen && overallStatus == false && appConfig && haveSetText) { - //trying to block early cases from throwing modal - window.appStatusModalOpened = true; - setPermitVis(true); + console.log("Explanation = "+explanationList); } - }, [overallStatus]); - - useAppStateChange(function () { - console.log('PERMISSION CHECK: app has resumed, should refresh'); - refreshAllChecks(); - }); - - //load when ready - useEffect(() => { - if (appConfig && window['device']?.platform) { - platform.current = window['device'].platform.toLowerCase(); - osver.current = window['device'].version.split('.')[0]; - - if (!haveSetText) { - //window.appStatusModalOpened = false; - setupPermissionText(); - setHaveSetText(true); - } - if (!checkList || checkList.length == 0) { - console.log('setting up permissions'); - createChecklist(); - } + + function createChecklist(){ + console.debug("setting up checks, platform is " + platform.current + "and osver is " + osver.current); + if(platform.current == "android") { + setupAndroidLocChecks(); + setupAndroidFitnessChecks(); + setupAndroidNotificationChecks(); + setupAndroidBackgroundRestrictionChecks(); + } else if (platform.current == "ios") { + setupIOSLocChecks(); + setupIOSFitnessChecks(); + setupAndroidNotificationChecks(); + } else { + setError("Alert! unknownplatform, no tracking"); + setErrorVis(true); + console.log("Alert! unknownplatform, no tracking"); //need an alert, can use AlertBar? + } + + refreshAllChecks(); } - }, [appConfig]); - useEffect(() => { - if (!permitVis) { - window.appStatusModalOpened = false; + //refreshing checks with the plugins to update the check's statusState + function refreshAllChecks() { + //refresh each check + checkList.forEach((lc) => { + lc.refresh(); + }); + console.log("setting checks are", checkList); } - }, [permitVis]); - - //anytime the checks change (mostly when refreshed), recompute the visual pieces - useEffect(() => { - console.log('checklist changed, updating', checkList); - recomputeAllChecks(); - }, [checkList]); - - return ( - <> - setPermitVis(false)} transparent={true}> - setPermitVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('consent.permissions')} - - - {t('intro.appstatus.overall-description')} - - - {checkList?.map((lc) => )} - - - - - - - - - - - - ); -}; + + //recomputing checks updates the visual cues of their status + function recomputeAllChecks() { + console.log("recomputing checks", checkList); + checkList.forEach((lc) => { + lc.statusIcon = iconMap(lc.statusState); + lc.statusColor = colorMap(lc.statusState) + }); + } + + //anytime the status changes, may need to show modal + useEffect(() => { + let currentlyOpen = window?.appStatusModalOpened; + if(!currentlyOpen && overallStatus == false && appConfig && haveSetText) { //trying to block early cases from throwing modal + window.appStatusModalOpened = true; + setPermitVis(true); + } + }, [overallStatus]); + + useAppStateChange( function() { + console.log("PERMISSION CHECK: app has resumed, should refresh"); + refreshAllChecks(); + }); + + //load when ready + useEffect(() => { + if (appConfig && window['device']?.platform) { + platform.current = window['device'].platform.toLowerCase(); + osver.current = window['device'].version.split(".")[0]; + + if(!haveSetText) + { + //window.appStatusModalOpened = false; + setupPermissionText(); + setHaveSetText(true); + } + if(!checkList || checkList.length == 0) { + console.log("setting up permissions"); + createChecklist(); + } + + } + }, [appConfig]); + + useEffect (() => { + if(!permitVis) { + window.appStatusModalOpened = false; + } + }, [permitVis]); + + //anytime the checks change (mostly when refreshed), recompute the visual pieces + useEffect(() => { + console.log("checklist changed, updating", checkList); + recomputeAllChecks(); + }, [checkList]) + + return ( + <> + setPermitVis(false)} transparent={true}> + setPermitVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('consent.permissions')} + + + {t('intro.appstatus.overall-description')} + + + {checkList?.map((lc) => + + + )} + + + + + + + + + + + + ) +} export default AppStatusModal; diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index 11bc5dfc4..99318b1ac 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -1,363 +1,285 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, View } from 'react-native'; +import React, { useEffect, useState } from "react"; +import { Modal, View } from "react-native"; import { Dialog, Button, Switch, Text, useTheme, TextInput } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; -import ActionMenu from '../components/ActionMenu'; -import { settingStyles } from './ProfileSettings'; -import { getAngularService } from '../angular-react-helper'; +import { useTranslation } from "react-i18next"; +import ActionMenu from "../components/ActionMenu"; +import { settingStyles } from "./ProfileSettings"; +import { getAngularService } from "../angular-react-helper"; -type collectionConfig = { - is_duty_cycling: boolean; - simulate_user_interaction: boolean; - accuracy: number; - accuracy_threshold: number; - filter_distance: number; - filter_time: number; - geofence_radius: number; - ios_use_visit_notifications_for_detection: boolean; - ios_use_remote_push_for_sync: boolean; - android_geofence_responsiveness: number; +type collectionConfig = { + is_duty_cycling: boolean, + simulate_user_interaction: boolean, + accuracy: number, + accuracy_threshold: number, + filter_distance: number, + filter_time: number, + geofence_radius: number, + ios_use_visit_notifications_for_detection: boolean, + ios_use_remote_push_for_sync: boolean, + android_geofence_responsiveness: number }; export async function forceTransition(transition) { - try { - let result = forceTransitionWrapper(transition); - window.alert('success -> ' + result); - } catch (err) { - window.alert('error -> ' + err); - console.log('error forcing state', err); - } + try { + let result = forceTransitionWrapper(transition); + window.alert('success -> '+result); + } catch (err) { + window.alert('error -> '+err); + console.log("error forcing state", err); + } } async function accuracy2String(config) { - var accuracy = config.accuracy; - let accuracyOptions = await getAccuracyOptions(); - for (var k in accuracyOptions) { - if (accuracyOptions[k] == accuracy) { - return k; + var accuracy = config.accuracy; + let accuracyOptions = await getAccuracyOptions(); + for (var k in accuracyOptions) { + if (accuracyOptions[k] == accuracy) { + return k; + } } - } - return accuracy; + return accuracy; } export async function isMediumAccuracy() { - let config = await getConfig(); - if (!config || config == null) { - return undefined; // config not loaded when loading ui, set default as false - } else { - var v = await accuracy2String(config); - console.log('window platform is', window['cordova'].platformId); - if (window['cordova'].platformId == 'ios') { - return ( - v != 'kCLLocationAccuracyBestForNavigation' && - v != 'kCLLocationAccuracyBest' && - v != 'kCLLocationAccuracyTenMeters' - ); - } else if (window['cordova'].platformId == 'android') { - return v != 'PRIORITY_HIGH_ACCURACY'; + let config = await getConfig(); + if (!config || config == null) { + return undefined; // config not loaded when loading ui, set default as false } else { - window.alert('Emission does not support this platform'); + var v = await accuracy2String(config); + console.log("window platform is", window['cordova'].platformId); + if (window['cordova'].platformId == 'ios') { + return v != "kCLLocationAccuracyBestForNavigation" && v != "kCLLocationAccuracyBest" && v != "kCLLocationAccuracyTenMeters"; + } else if (window['cordova'].platformId == 'android') { + return v != "PRIORITY_HIGH_ACCURACY"; + } else { + window.alert("Emission does not support this platform"); + } } - } } export async function helperToggleLowAccuracy() { - const Logger = getAngularService('Logger'); - let tempConfig = await getConfig(); - let accuracyOptions = await getAccuracyOptions(); - let medium = await isMediumAccuracy(); - if (medium) { - if (window['cordova'].platformId == 'ios') { - tempConfig.accuracy = accuracyOptions['kCLLocationAccuracyBest']; - } else if (window['cordova'].platformId == 'android') { - tempConfig.accuracy = accuracyOptions['PRIORITY_HIGH_ACCURACY']; + const Logger = getAngularService("Logger"); + let tempConfig = await getConfig(); + let accuracyOptions = await getAccuracyOptions(); + let medium = await isMediumAccuracy(); + if (medium) { + if (window['cordova'].platformId == 'ios') { + tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyBest"]; + } else if (window['cordova'].platformId == 'android') { + tempConfig.accuracy = accuracyOptions["PRIORITY_HIGH_ACCURACY"]; + } + } else { + if (window['cordova'].platformId == 'ios') { + tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyHundredMeters"]; + } else if (window['cordova'].platformId == 'android') { + tempConfig.accuracy = accuracyOptions["PRIORITY_BALANCED_POWER_ACCURACY"]; + } } - } else { - if (window['cordova'].platformId == 'ios') { - tempConfig.accuracy = accuracyOptions['kCLLocationAccuracyHundredMeters']; - } else if (window['cordova'].platformId == 'android') { - tempConfig.accuracy = accuracyOptions['PRIORITY_BALANCED_POWER_ACCURACY']; + try{ + let set = await setConfig(tempConfig); + console.log("setConfig Sucess"); + } catch (err) { + Logger.displayError("Error while setting collection config", err); } - } - try { - let set = await setConfig(tempConfig); - console.log('setConfig Sucess'); - } catch (err) { - Logger.displayError('Error while setting collection config', err); - } } /* - * Simple read/write wrappers - */ +* Simple read/write wrappers +*/ -export const getState = function () { - return window['cordova'].plugins.BEMDataCollection.getState(); +export const getState = function() { + return window['cordova'].plugins.BEMDataCollection.getState(); }; export async function getHelperCollectionSettings() { - let promiseList = []; - promiseList.push(getConfig()); - promiseList.push(getAccuracyOptions()); - let resultList = await Promise.all(promiseList); - let tempConfig = resultList[0]; - let tempAccuracyOptions = resultList[1]; - return formatConfigForDisplay(tempConfig, tempAccuracyOptions); + let promiseList = []; + promiseList.push(getConfig()); + promiseList.push(getAccuracyOptions()); + let resultList = await Promise.all(promiseList); + let tempConfig = resultList[0]; + let tempAccuracyOptions = resultList[1]; + return formatConfigForDisplay(tempConfig, tempAccuracyOptions); } -const setConfig = function (config) { - return window['cordova'].plugins.BEMDataCollection.setConfig(config); +const setConfig = function(config) { + return window['cordova'].plugins.BEMDataCollection.setConfig(config); }; -const getConfig = function () { - return window['cordova'].plugins.BEMDataCollection.getConfig(); +const getConfig = function() { + return window['cordova'].plugins.BEMDataCollection.getConfig(); }; -const getAccuracyOptions = function () { - return window['cordova'].plugins.BEMDataCollection.getAccuracyOptions(); +const getAccuracyOptions = function() { + return window['cordova'].plugins.BEMDataCollection.getAccuracyOptions(); }; -export const forceTransitionWrapper = function (transition) { - return window['cordova'].plugins.BEMDataCollection.forceTransition(transition); +export const forceTransitionWrapper = function(transition) { + return window['cordova'].plugins.BEMDataCollection.forceTransition(transition); }; -const formatConfigForDisplay = function (config, accuracyOptions) { - var retVal = []; - for (var prop in config) { - if (prop == 'accuracy') { - for (var name in accuracyOptions) { - if (accuracyOptions[name] == config[prop]) { - retVal.push({ key: prop, val: name }); +const formatConfigForDisplay = function(config, accuracyOptions) { + var retVal = []; + for (var prop in config) { + if (prop == "accuracy") { + for (var name in accuracyOptions) { + if (accuracyOptions[name] == config[prop]) { + retVal.push({'key': prop, 'val': name}); + } + } + } else { + retVal.push({'key': prop, 'val': config[prop]}); } - } - } else { - retVal.push({ key: prop, val: config[prop] }); } - } - return retVal; -}; + return retVal; +} const ControlSyncHelper = ({ editVis, setEditVis }) => { - const { colors } = useTheme(); - const Logger = getAngularService('Logger'); - - const [localConfig, setLocalConfig] = useState(); - const [accuracyActions, setAccuracyActions] = useState([]); - const [accuracyVis, setAccuracyVis] = useState(false); + const {colors} = useTheme(); + const Logger = getAngularService("Logger"); - async function getCollectionSettings() { - let promiseList = []; - promiseList.push(getConfig()); - promiseList.push(getAccuracyOptions()); - let resultList = await Promise.all(promiseList); - let tempConfig = resultList[0]; - setLocalConfig(tempConfig); - let tempAccuracyOptions = resultList[1]; - setAccuracyActions(formatAccuracyForActions(tempAccuracyOptions)); - return formatConfigForDisplay(tempConfig, tempAccuracyOptions); - } + const [ localConfig, setLocalConfig ] = useState(); + const [ accuracyActions, setAccuracyActions ] = useState([]); + const [ accuracyVis, setAccuracyVis ] = useState(false); + + async function getCollectionSettings() { + let promiseList = []; + promiseList.push(getConfig()); + promiseList.push(getAccuracyOptions()); + let resultList = await Promise.all(promiseList); + let tempConfig = resultList[0]; + setLocalConfig(tempConfig); + let tempAccuracyOptions = resultList[1]; + setAccuracyActions(formatAccuracyForActions(tempAccuracyOptions)); + return formatConfigForDisplay(tempConfig, tempAccuracyOptions); + } - useEffect(() => { - getCollectionSettings(); - }, [editVis]); + useEffect(() => { + getCollectionSettings(); + }, [editVis]) - const formatAccuracyForActions = function (accuracyOptions) { - let tempAccuracyActions = []; - for (var name in accuracyOptions) { - tempAccuracyActions.push({ text: name, value: accuracyOptions[name] }); + const formatAccuracyForActions = function(accuracyOptions) { + let tempAccuracyActions = []; + for (var name in accuracyOptions) { + tempAccuracyActions.push({text: name, value: accuracyOptions[name]}); + } + return tempAccuracyActions; } - return tempAccuracyActions; - }; - /* - * Functions to edit and save values - */ + /* + * Functions to edit and save values + */ - async function saveAndReload() { - console.log('new config = ', localConfig); - try { - let set = await setConfig(localConfig); - //TODO find way to not need control.update.complete event broadcast - } catch (err) { - Logger.displayError('Error while setting collection config', err); + async function saveAndReload() { + console.log("new config = ", localConfig); + try{ + let set = await setConfig(localConfig); + //TODO find way to not need control.update.complete event broadcast + } catch(err) { + Logger.displayError("Error while setting collection config", err); + } } - } - const onToggle = function (config_key) { - let tempConfig = { ...localConfig }; - tempConfig[config_key] = !localConfig[config_key]; - setLocalConfig(tempConfig); - }; + const onToggle = function(config_key) { + let tempConfig = {...localConfig}; + tempConfig[config_key] = !localConfig[config_key]; + setLocalConfig(tempConfig); + } - const onChooseAccuracy = function (accuracyOption) { - let tempConfig = { ...localConfig }; - tempConfig.accuracy = accuracyOption.value; - setLocalConfig(tempConfig); - }; + const onChooseAccuracy = function(accuracyOption) { + let tempConfig = {...localConfig}; + tempConfig.accuracy = accuracyOption.value; + setLocalConfig(tempConfig); + } - const onChangeText = function (newText, config_key) { - let tempConfig = { ...localConfig }; - tempConfig[config_key] = parseInt(newText); - setLocalConfig(tempConfig); - }; + const onChangeText = function(newText, config_key) { + let tempConfig = {...localConfig}; + tempConfig[config_key] = parseInt(newText); + setLocalConfig(tempConfig); + } - /*ios vs android*/ - let filterComponent; - if (window['cordova'].platformId == 'ios') { - filterComponent = ( - - Filter Distance - onChangeText(text, 'filter_distance')} - /> - - ); - } else { - filterComponent = ( - - Filter Interval - onChangeText(text, 'filter_time')} - /> - - ); - } - let iosToggles; - if (window['cordova'].platformId == 'ios') { - iosToggles = ( - <> + /*ios vs android*/ + let filterComponent; + if(window['cordova'].platformId == 'ios') { + filterComponent = + Filter Distance + onChangeText(text, "filter_distance")}/> + + } else { + filterComponent = + Filter Interval + onChangeText(text, "filter_time")}/> + + } + let iosToggles; + if(window['cordova'].platformId == 'ios') { + iosToggles = <> {/* use visit notifications toggle NO ANDROID */} - - Use Visit Notifications - onToggle('ios_use_visit_notifications_for_detection')}> + + Use Visit Notifications + onToggle("ios_use_visit_notifications_for_detection")}> {/* sync on remote push toggle NO ANDROID */} - - Sync on remote push - onToggle('ios_use_remote_push_for_sync}')}> + + Sync on remote push + onToggle("ios_use_remote_push_for_sync}")}> - - ); - } - let geofenceComponent; - if (window['cordova'].platformId == 'android') { - geofenceComponent = ( - - Geofence Responsiveness - onChangeText(text, 'android_geofence_responsiveness')} - /> - - ); - } - - return ( - <> - setEditVis(false)} transparent={true}> - setEditVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - Edit Collection Settings - - {/* duty cycling toggle */} - - Duty Cycling - onToggle('is_duty_cycling')}> - - {/* simulate user toggle */} - - Simulate User - onToggle('simulate_user_interaction')}> - - {/* accuracy */} - - Accuracy - - - {/* accuracy threshold not editable*/} - - Accuracy Threshold - {localConfig?.accuracy_threshold} - - {filterComponent} - {/* geofence radius */} - - Geofence Radius - onChangeText(text, 'geofence_radius')} - /> - - {iosToggles} - {geofenceComponent} - - - - - - - + + } + let geofenceComponent; + if(window['cordova'].platformId == 'android') { + geofenceComponent = + Geofence Responsiveness + onChangeText(text, "android_geofence_responsiveness")}/> + + } - {}}> - - ); -}; + return ( + <> + setEditVis(false)} transparent={true}> + setEditVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> + Edit Collection Settings + + {/* duty cycling toggle */} + + Duty Cycling + onToggle("is_duty_cycling")}> + + {/* simulate user toggle */} + + Simulate User + onToggle("simulate_user_interaction")}> + + {/* accuracy */} + + Accuracy + + + {/* accuracy threshold not editable*/} + + Accuracy Threshold + {localConfig?.accuracy_threshold} + + {filterComponent} + {/* geofence radius */} + + Geofence Radius + onChangeText(text, "geofence_radius")}/> + + {iosToggles} + {geofenceComponent} + + + + + + + + {}}> + + ); + }; + export default ControlSyncHelper; diff --git a/www/js/control/ControlDataTable.jsx b/www/js/control/ControlDataTable.jsx index 932762400..796b057ec 100644 --- a/www/js/control/ControlDataTable.jsx +++ b/www/js/control/ControlDataTable.jsx @@ -1,18 +1,18 @@ -import React from 'react'; +import React from "react"; import { DataTable } from 'react-native-paper'; // 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! - {controlData?.map((e) => ( - + {controlData?.map((e) => + {e.key} {e.val.toString()} - ))} + )} ); }; @@ -23,7 +23,7 @@ const styles = { borderColor: 'rgba(0,0,0,0.25)', borderLeftWidth: 15, borderLeftColor: 'rgba(0,0,0,0.25)', - }, -}; + } +} export default ControlDataTable; diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index 30fc27c15..490672c4d 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -1,317 +1,284 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, View } from 'react-native'; +import React, { useEffect, useState } from "react"; +import { Modal, View } from "react-native"; import { Dialog, Button, Switch, Text, useTheme } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; -import { settingStyles } from './ProfileSettings'; -import { getAngularService } from '../angular-react-helper'; -import ActionMenu from '../components/ActionMenu'; -import SettingRow from './SettingRow'; -import AlertBar from './AlertBar'; -import moment from 'moment'; +import { useTranslation } from "react-i18next"; +import { settingStyles } from "./ProfileSettings"; +import { getAngularService } from "../angular-react-helper"; +import ActionMenu from "../components/ActionMenu"; +import SettingRow from "./SettingRow"; +import AlertBar from "./AlertBar"; +import moment from "moment"; /* - * BEGIN: Simple read/write wrappers - */ +* BEGIN: Simple read/write wrappers +*/ export function forcePluginSync() { - return window.cordova.plugins.BEMServerSync.forceSync(); -} + return window.cordova.plugins.BEMServerSync.forceSync(); +}; const formatConfigForDisplay = (configToFormat) => { - var formatted = []; - for (let prop in configToFormat) { - formatted.push({ key: prop, val: configToFormat[prop] }); - } - return formatted; -}; + var formatted = []; + for (let prop in configToFormat) { + formatted.push({'key': prop, 'val': configToFormat[prop]}); + } + return formatted; +} -const setConfig = function (config) { - return window.cordova.plugins.BEMServerSync.setConfig(config); -}; +const setConfig = function(config) { + return window.cordova.plugins.BEMServerSync.setConfig(config); + }; -const getConfig = function () { - return window.cordova.plugins.BEMServerSync.getConfig(); +const getConfig = function() { + return window.cordova.plugins.BEMServerSync.getConfig(); }; export async function getHelperSyncSettings() { - let tempConfig = await getConfig(); - return formatConfigForDisplay(tempConfig); + let tempConfig = await getConfig(); + return formatConfigForDisplay(tempConfig); } -const getEndTransitionKey = function () { - if (window.cordova.platformId == 'android') { - return 'local.transition.stopped_moving'; - } else if (window.cordova.platformId == 'ios') { - return 'T_TRIP_ENDED'; - } -}; +const getEndTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.stopped_moving"; + } + else if(window.cordova.platformId == 'ios') { + return "T_TRIP_ENDED"; + } +} -type syncConfig = { sync_interval: number; ios_use_remote_push: boolean }; +type syncConfig = { sync_interval: number, + ios_use_remote_push: boolean }; //forceSync and endForceSync SettingRows & their actions -export const ForceSyncRow = ({ getState }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const ClientStats = getAngularService('ClientStats'); - const Logger = getAngularService('Logger'); - - const [dataPendingVis, setDataPendingVis] = useState(false); - const [dataPushedVis, setDataPushedVis] = useState(false); - - async function forceSync() { - try { - let addedEvent = ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC); - console.log('Added ' + ClientStats.getStatKeys().BUTTON_FORCE_SYNC + ' event'); - - let sync = await forcePluginSync(); - /* - * Change to sensorKey to "background/location" after fixing issues - * with getLastSensorData and getLastMessages in the usercache - * See https://github.com/e-mission/e-mission-phone/issues/279 for details - */ - var sensorKey = 'statemachine/transition'; - let sensorDataList = await window.cordova.plugins.BEMUserCache.getAllMessages( - sensorKey, - true, - ); - - // If everything has been pushed, we should - // have no more trip end transitions left - let isTripEnd = function (entry) { - return entry.metadata == getEndTransitionKey(); - }; - let syncLaunchedCalls = sensorDataList.filter(isTripEnd); - let syncPending = syncLaunchedCalls.length > 0; - Logger.log( - 'sensorDataList.length = ' + - sensorDataList.length + - ', syncLaunchedCalls.length = ' + - syncLaunchedCalls.length + - ', syncPending? = ' + - syncPending, - ); - Logger.log('sync launched = ' + syncPending); - - if (syncPending) { - Logger.log(Logger.log('data is pending, showing confirm dialog')); - setDataPendingVis(true); //consent handling in modal - } else { - setDataPushedVis(true); - } - } catch (error) { - Logger.displayError('Error while forcing sync', error); +export const ForceSyncRow = ({getState}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const ClientStats = getAngularService('ClientStats'); + const Logger = getAngularService('Logger'); + + const [dataPendingVis, setDataPendingVis] = useState(false); + const [dataPushedVis, setDataPushedVis] = useState(false); + + async function forceSync() { + try { + let addedEvent = ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC); + console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); + + let sync = await forcePluginSync(); + /* + * Change to sensorKey to "background/location" after fixing issues + * with getLastSensorData and getLastMessages in the usercache + * See https://github.com/e-mission/e-mission-phone/issues/279 for details + */ + var sensorKey = "statemachine/transition"; + let sensorDataList = await window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); + + // If everything has been pushed, we should + // have no more trip end transitions left + let isTripEnd = function(entry) { + return entry.metadata == getEndTransitionKey(); + } + let syncLaunchedCalls = sensorDataList.filter(isTripEnd); + let syncPending = syncLaunchedCalls.length > 0; + Logger.log("sensorDataList.length = "+sensorDataList.length+ + ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ + ", syncPending? = "+syncPending); + Logger.log("sync launched = "+syncPending); + + if(syncPending) { + Logger.log(Logger.log("data is pending, showing confirm dialog")); + setDataPendingVis(true); //consent handling in modal + } else { + setDataPushedVis(true); + } + } catch (error) { + Logger.displayError("Error while forcing sync", error); + } + }; + + const getStartTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.exited_geofence"; + } + else if(window.cordova.platformId == 'ios') { + return "T_EXITED_GEOFENCE"; + } } - } - const getStartTransitionKey = function () { - if (window.cordova.platformId == 'android') { - return 'local.transition.exited_geofence'; - } else if (window.cordova.platformId == 'ios') { - return 'T_EXITED_GEOFENCE'; + const getEndTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.stopped_moving"; + } + else if(window.cordova.platformId == 'ios') { + return "T_TRIP_ENDED"; + } } - }; - const getEndTransitionKey = function () { - if (window.cordova.platformId == 'android') { - return 'local.transition.stopped_moving'; - } else if (window.cordova.platformId == 'ios') { - return 'T_TRIP_ENDED'; + const getOngoingTransitionState = function() { + if(window.cordova.platformId == 'android') { + return "local.state.ongoing_trip"; + } + else if(window.cordova.platformId == 'ios') { + return "STATE_ONGOING_TRIP"; + } } - }; - - const getOngoingTransitionState = function () { - if (window.cordova.platformId == 'android') { - return 'local.state.ongoing_trip'; - } else if (window.cordova.platformId == 'ios') { - return 'STATE_ONGOING_TRIP'; - } - }; - async function getTransition(transKey) { - var entry_data = {}; - const curr_state = await getState(); - entry_data.curr_state = curr_state; - if (transKey == getEndTransitionKey()) { - entry_data.curr_state = getOngoingTransitionState(); + async function getTransition(transKey) { + var entry_data = {}; + const curr_state = await getState(); + entry_data.curr_state = curr_state; + if (transKey == getEndTransitionKey()) { + entry_data.curr_state = getOngoingTransitionState(); + } + entry_data.transition = transKey; + entry_data.ts = moment().unix(); + return entry_data; } - entry_data.transition = transKey; - entry_data.ts = moment().unix(); - return entry_data; - } - async function endForceSync() { - /* First, quickly start and end the trip. Let's listen to the promise - * result for start so that we ensure ordering */ - var sensorKey = 'statemachine/transition'; - let entry_data = await getTransition(getStartTransitionKey()); - let messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - entry_data = await getTransition(getEndTransitionKey()); - messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - forceSync(); - } - - return ( - <> - - - - {/* dataPending */} - setDataPendingVis(false)} transparent={true}> - setDataPendingVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('data pending for push')} - - - - - - - - - - ); -}; + async function endForceSync() { + /* First, quickly start and end the trip. Let's listen to the promise + * result for start so that we ensure ordering */ + var sensorKey = "statemachine/transition"; + let entry_data = await getTransition(getStartTransitionKey()); + let messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + entry_data = await getTransition(getEndTransitionKey()); + messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + forceSync(); + }; + + return ( + <> + + + + {/* dataPending */} + setDataPendingVis(false)} transparent={true}> + setDataPendingVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('data pending for push')} + + + + + + + + + + ) +} //UI for editing the sync config const ControlSyncHelper = ({ editVis, setEditVis }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const CommHelper = getAngularService('CommHelper'); - const Logger = getAngularService('Logger'); - - const [localConfig, setLocalConfig] = useState(); - const [intervalVis, setIntervalVis] = useState(false); - - /* - * Functions to read and format values for display - */ - async function getSyncSettings() { - let tempConfig = await getConfig(); - setLocalConfig(tempConfig); - } - - useEffect(() => { - getSyncSettings(); - }, [editVis]); - - const syncIntervalActions = [ - { text: '1 min', value: 60 }, - { text: '10 min', value: 10 * 60 }, - { text: '30 min', value: 30 * 60 }, - { text: '1 hr', value: 60 * 60 }, - ]; + const { t } = useTranslation(); + const { colors } = useTheme(); + const CommHelper = getAngularService("CommHelper"); + const Logger = getAngularService("Logger"); + + const [ localConfig, setLocalConfig ] = useState(); + const [ intervalVis, setIntervalVis ] = useState(false); + + /* + * Functions to read and format values for display + */ + async function getSyncSettings() { + let tempConfig = await getConfig(); + setLocalConfig(tempConfig); + } - /* - * Functions to edit and save values - */ - async function saveAndReload() { - console.log('new config = ' + localConfig); - try { - let set = setConfig(localConfig); - //NOTE -- we need to make sure we update these settings in ProfileSettings :) -- getting rid of broadcast handling for migration!! - CommHelper.updateUser({ - // TODO: worth thinking about where best to set this - // Currently happens in native code. Now that we are switching - // away from parse, we can store this from javascript here. - // or continue to store from native - // this is easier for people to see, but means that calls to - // native, even through the javascript interface are not complete - curr_sync_interval: localConfig.sync_interval, - }); - } catch (err) { - console.log('error with setting sync config', err); - Logger.displayError('Error while setting sync config', err); + useEffect(() => { + getSyncSettings(); + }, [editVis]) + + const syncIntervalActions = [ + {text: "1 min", value: 60}, + {text: "10 min", value: 10 * 60}, + {text: "30 min", value: 30 * 60}, + {text: "1 hr", value: 60 * 60} + ] + + /* + * Functions to edit and save values + */ + async function saveAndReload() { + console.log("new config = "+localConfig); + try{ + let set = setConfig(localConfig); + //NOTE -- we need to make sure we update these settings in ProfileSettings :) -- getting rid of broadcast handling for migration!! + CommHelper.updateUser({ + // TODO: worth thinking about where best to set this + // Currently happens in native code. Now that we are switching + // away from parse, we can store this from javascript here. + // or continue to store from native + // this is easier for people to see, but means that calls to + // native, even through the javascript interface are not complete + curr_sync_interval: localConfig.sync_interval + }); + } catch (err) + { + console.log("error with setting sync config", err); + Logger.displayError("Error while setting sync config", err); + } } - } - const onChooseInterval = function (interval) { - let tempConfig = { ...localConfig }; - tempConfig.sync_interval = interval.value; - setLocalConfig(tempConfig); - }; + const onChooseInterval = function(interval) { + let tempConfig = {...localConfig}; + tempConfig.sync_interval = interval.value; + setLocalConfig(tempConfig); + } - const onTogglePush = function () { - let tempConfig = { ...localConfig }; - tempConfig.ios_use_remote_push = !localConfig.ios_use_remote_push; - setLocalConfig(tempConfig); - }; + const onTogglePush = function() { + let tempConfig = {...localConfig}; + tempConfig.ios_use_remote_push = !localConfig.ios_use_remote_push; + setLocalConfig(tempConfig); + } - /* - * configure the UI - */ - let toggle; - if (window.cordova.platformId == 'ios') { - toggle = ( - - Use Remote Push - - + /* + * configure the UI + */ + let toggle; + if(window.cordova.platformId == 'ios'){ + toggle = + Use Remote Push + + + } + + return ( + <> + {/* popup to show when we want to edit */} + setEditVis(false)} transparent={true}> + setEditVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> + Edit Sync Settings + + + Sync Interval + + + {toggle} + + + + + + + + + {}}> + ); - } - - return ( - <> - {/* popup to show when we want to edit */} - setEditVis(false)} transparent={true}> - setEditVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - Edit Sync Settings - - - Sync Interval - - - {toggle} - - - - - - - - - {}}> - - ); -}; - -export default ControlSyncHelper; + }; + +export default ControlSyncHelper; \ No newline at end of file diff --git a/www/js/control/DataDatePicker.tsx b/www/js/control/DataDatePicker.tsx index 7f143f3bd..83e0986b2 100644 --- a/www/js/control/DataDatePicker.tsx +++ b/www/js/control/DataDatePicker.tsx @@ -1,14 +1,14 @@ // this date picker element is set up to handle the "download data from day" in ProfileSettings // it relies on an angular service (Control Helper) but when we migrate that we might want to download a range instead of single -import React from 'react'; +import React from "react"; import { DatePickerModal } from 'react-native-paper-dates'; -import { useTranslation } from 'react-i18next'; -import { getAngularService } from '../angular-react-helper'; +import { useTranslation } from "react-i18next"; +import { getAngularService } from "../angular-react-helper"; -const DataDatePicker = ({ date, setDate, open, setOpen, minDate }) => { +const DataDatePicker = ({date, setDate, open, setOpen, minDate}) => { const { t, i18n } = useTranslation(); //able to pull lang from this - const ControlHelper = getAngularService('ControlHelper'); + const ControlHelper = getAngularService("ControlHelper"); const onDismiss = React.useCallback(() => { setOpen(false); @@ -20,27 +20,27 @@ const DataDatePicker = ({ date, setDate, open, setOpen, minDate }) => { setDate(params.date); ControlHelper.getMyData(params.date); }, - [setOpen, setDate], + [setOpen, setDate] ); const maxDate = new Date(); return ( <> - + ); -}; +} -export default DataDatePicker; +export default DataDatePicker; \ No newline at end of file diff --git a/www/js/control/DemographicsSettingRow.jsx b/www/js/control/DemographicsSettingRow.jsx index e8f5095f6..ff91d0921 100644 --- a/www/js/control/DemographicsSettingRow.jsx +++ b/www/js/control/DemographicsSettingRow.jsx @@ -1,8 +1,9 @@ -import React from 'react'; -import { getAngularService } from '../angular-react-helper'; -import SettingRow from './SettingRow'; +import React from "react"; +import { getAngularService } from "../angular-react-helper"; +import SettingRow from "./SettingRow"; + +const DemographicsSettingRow = ({ }) => { -const DemographicsSettingRow = ({}) => { const EnketoDemographicsService = getAngularService('EnketoDemographicsService'); const EnketoSurveyLaunch = getAngularService('EnketoSurveyLaunch'); const $rootScope = getAngularService('$rootScope'); @@ -10,24 +11,19 @@ const DemographicsSettingRow = ({}) => { // 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 EnketoSurveyLaunch + .launch($rootScope, 'UserProfileSurvey', { + prefilledSurveyResponse: lastSurvey?.data?.xmlResponse, + showBackButton: true, showFormFooterJumpNav: true + }) + .then(result => { + console.log("demographic survey result ", result); + }); }); } - return ( - - ); + return }; -export default DemographicsSettingRow; +export default DemographicsSettingRow; \ No newline at end of file diff --git a/www/js/control/ExpandMenu.jsx b/www/js/control/ExpandMenu.jsx index 65c2fb3b3..2f8bb8ef1 100644 --- a/www/js/control/ExpandMenu.jsx +++ b/www/js/control/ExpandMenu.jsx @@ -1,15 +1,15 @@ -import React from 'react'; +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'; +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 { 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); + const handlePress = () => setExpanded(!expanded); return ( { titleStyle={rowStyles.title} expanded={expanded} onPress={handlePress}> - {props.children} + {props.children} ); }; const styles = StyleSheet.create({ section: (surfaceColor) => ({ - justifyContent: 'space-between', - backgroundColor: surfaceColor, - margin: 1, + justifyContent: 'space-between', + backgroundColor: surfaceColor, + margin: 1, }), }); -export default ExpansionSection; +export default ExpansionSection; \ No newline at end of file diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 99c55bd28..7d6d279ee 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -1,183 +1,153 @@ -import React, { useState, useMemo, useEffect } from 'react'; -import { View, StyleSheet, SafeAreaView, Modal } from 'react-native'; -import { useTheme, Text, Appbar, IconButton } from 'react-native-paper'; -import { getAngularService } from '../angular-react-helper'; -import { useTranslation } from 'react-i18next'; +import React, { useState, useMemo, useEffect } from "react"; +import { View, StyleSheet, SafeAreaView, Modal } from "react-native"; +import { useTheme, Text, Appbar, IconButton } from "react-native-paper"; +import { getAngularService } from "../angular-react-helper"; +import { useTranslation } from "react-i18next"; import { FlashList } from '@shopify/flash-list'; -import moment from 'moment'; -import AlertBar from './AlertBar'; - -type loadStats = { currentStart: number; gotMaxIndex: boolean; reachedEnd: boolean }; - -const LogPage = ({ pageVis, setPageVis }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const EmailHelper = getAngularService('EmailHelper'); - - const [loadStats, setLoadStats] = useState(); - const [entries, setEntries] = useState([]); - const [maxErrorVis, setMaxErrorVis] = useState(false); - const [logErrorVis, setLogErrorVis] = useState(false); - const [maxMessage, setMaxMessage] = useState(''); - const [logMessage, setLogMessage] = useState(''); - const [isFetching, setIsFetching] = useState(false); - - var RETRIEVE_COUNT = 100; - - //when opening the modal, load the entries - useEffect(() => { - refreshEntries(); - }, [pageVis]); - - async function refreshEntries() { - try { - let maxIndex = await window.Logger.getMaxIndex(); - console.log('maxIndex = ' + maxIndex); - let tempStats = {} as loadStats; - tempStats.currentStart = maxIndex; - tempStats.gotMaxIndex = true; - tempStats.reachedEnd = false; - setLoadStats(tempStats); - setEntries([]); - } catch (error) { - let errorString = t('errors.while-max-index') + JSON.stringify(error, null, 2); - console.log(errorString); - setMaxMessage(errorString); - setMaxErrorVis(true); - } finally { - addEntries(); +import moment from "moment"; +import AlertBar from "./AlertBar"; + +type loadStats = { currentStart: number, gotMaxIndex: boolean, reachedEnd: boolean }; + +const LogPage = ({pageVis, setPageVis}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const EmailHelper = getAngularService('EmailHelper'); + + const [ loadStats, setLoadStats ] = useState(); + const [ entries, setEntries ] = useState([]); + const [ maxErrorVis, setMaxErrorVis ] = useState(false); + const [ logErrorVis, setLogErrorVis ] = useState(false); + const [ maxMessage, setMaxMessage ] = useState(""); + const [ logMessage, setLogMessage ] = useState(""); + const [ isFetching, setIsFetching ] = useState(false); + + var RETRIEVE_COUNT = 100; + + //when opening the modal, load the entries + useEffect(() => { + refreshEntries(); + }, [pageVis]); + + async function refreshEntries() { + try { + let maxIndex = await window.Logger.getMaxIndex(); + console.log("maxIndex = "+maxIndex); + let tempStats = {} as loadStats; + tempStats.currentStart = maxIndex; + tempStats.gotMaxIndex = true; + tempStats.reachedEnd = false; + setLoadStats(tempStats); + setEntries([]); + } catch(error) { + let errorString = t('errors.while-max-index')+JSON.stringify(error, null, 2); + console.log(errorString); + setMaxMessage(errorString); + setMaxErrorVis(true); + } finally { + addEntries(); + } } - } - - const moreDataCanBeLoaded = useMemo(() => { - return loadStats?.gotMaxIndex && !loadStats?.reachedEnd; - }, [loadStats]); - - const clear = function () { - window?.Logger.clearAll(); - window?.Logger.log(window.Logger.LEVEL_INFO, 'Finished clearing entries from unified log'); - refreshEntries(); - }; - - async function addEntries() { - console.log('calling addEntries'); - setIsFetching(true); - let start = loadStats.currentStart ? loadStats.currentStart : 0; //set a default start to prevent initial fetch error - try { - let entryList = await window.Logger.getMessagesFromIndex(start, RETRIEVE_COUNT); - processEntries(entryList); - console.log('entry list size = ' + entries.length); - setIsFetching(false); - } catch (error) { - let errStr = t('errors.while-log-messages') + JSON.stringify(error, null, 2); - console.log(errStr); - setLogMessage(errStr); - setLogErrorVis(true); - setIsFetching(false); + + const moreDataCanBeLoaded = useMemo(() => { + return loadStats?.gotMaxIndex && !loadStats?.reachedEnd; + }, [loadStats]) + + const clear = function() { + window?.Logger.clearAll(); + window?.Logger.log(window.Logger.LEVEL_INFO, "Finished clearing entries from unified log"); + refreshEntries(); + } + + async function addEntries() { + console.log("calling addEntries"); + setIsFetching(true); + let start = loadStats.currentStart ? loadStats.currentStart : 0; //set a default start to prevent initial fetch error + try { + let entryList = await window.Logger.getMessagesFromIndex(start, RETRIEVE_COUNT); + processEntries(entryList); + console.log("entry list size = "+ entries.length); + setIsFetching(false); + } catch(error) { + let errStr = t('errors.while-log-messages')+JSON.stringify(error, null, 2); + console.log(errStr); + setLogMessage(errStr); + setLogErrorVis(true); + setIsFetching(false); + } } - } - - const processEntries = function (entryList) { - let tempEntries = []; - let tempLoadStats = { ...loadStats }; - entryList.forEach((e) => { - e.fmt_time = moment.unix(e.ts).format('llll'); - tempEntries.push(e); - }); - if (entryList.length == 0) { - console.log('Reached the end of the scrolling'); - tempLoadStats.reachedEnd = true; - } else { - tempLoadStats.currentStart = entryList[entryList.length - 1].ID; - console.log('new start index = ' + loadStats.currentStart); + + const processEntries = function(entryList) { + let tempEntries = []; + let tempLoadStats = {...loadStats}; + entryList.forEach(e => { + e.fmt_time = moment.unix(e.ts).format("llll"); + tempEntries.push(e); + }); + if (entryList.length == 0) { + console.log("Reached the end of the scrolling"); + tempLoadStats.reachedEnd = true; + } else { + tempLoadStats.currentStart = entryList[entryList.length-1].ID; + console.log("new start index = "+loadStats.currentStart); + } + setEntries([...entries].concat(tempEntries)); //push the new entries onto the list + setLoadStats(tempLoadStats); + } + + const emailLog = function () { + EmailHelper.sendEmail("loggerDB"); } - setEntries([...entries].concat(tempEntries)); //push the new entries onto the list - setLoadStats(tempLoadStats); - }; - - const emailLog = function () { - EmailHelper.sendEmail('loggerDB'); - }; - - const separator = () => ; - const logItem = ({ item: logItem }) => ( - - - {logItem.fmt_time} - - - {logItem.ID + '|' + logItem.level + '|' + logItem.message} - - - ); - - return ( - setPageVis(false)}> - - - { - setPageVis(false); - }} - /> - - - - - refreshEntries()} /> - clear()} /> - emailLog()} /> - - - item.ID} - ItemSeparatorComponent={separator} - onEndReachedThreshold={0.5} - refreshing={isFetching} - onRefresh={() => { - if (moreDataCanBeLoaded) { - addEntries(); - } - }} - onEndReached={() => { - if (moreDataCanBeLoaded) { - addEntries(); - } - }} - /> - - - - - - ); + + const separator = () => + const logItem = ({item: logItem}) => ( + {logItem.fmt_time} + {logItem.ID + "|" + logItem.level + "|" + logItem.message} + ); + + return ( + setPageVis(false)}> + + + {setPageVis(false)}}/> + + + + + refreshEntries()}/> + clear()}/> + emailLog()}/> + + + item.ID} + ItemSeparatorComponent={separator} + onEndReachedThreshold={0.5} + refreshing={isFetching} + onRefresh={() => {if(moreDataCanBeLoaded){addEntries()}}} + onEndReached={() => {if(moreDataCanBeLoaded){addEntries()}}} + /> + + + + + + ); }; const styles = StyleSheet.create({ - date: (surfaceColor) => ({ - backgroundColor: surfaceColor, - }), - details: { - fontFamily: 'monospace', - }, - entry: (surfaceColor) => ({ - backgroundColor: surfaceColor, - marginLeft: 5, - }), -}); - -export default LogPage; + date: (surfaceColor) => ({ + backgroundColor: surfaceColor, + }), + details: { + fontFamily: "monospace", + }, + entry: (surfaceColor) => ({ + backgroundColor: surfaceColor, + marginLeft: 5, + }), + }); + +export default LogPage; \ No newline at end of file diff --git a/www/js/control/PopOpCode.jsx b/www/js/control/PopOpCode.jsx index 55cb1022d..721b3d511 100644 --- a/www/js/control/PopOpCode.jsx +++ b/www/js/control/PopOpCode.jsx @@ -1,92 +1,79 @@ -import React, { useState } from 'react'; +import React, { useState } 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'; -import AlertBar from './AlertBar'; -import { settingStyles } from './ProfileSettings'; +import { useTranslation } from "react-i18next"; +import QrCode from "../components/QrCode"; +import AlertBar from "./AlertBar"; +import { settingStyles } from "./ProfileSettings"; -const PopOpCode = ({ visibilityValue, tokenURL, action, setVis, dialogStyle }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); +const PopOpCode = ({visibilityValue, tokenURL, action, setVis, dialogStyle}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); - const opcodeList = tokenURL.split('='); - const opcode = opcodeList[opcodeList.length - 1]; + const opcodeList = tokenURL.split("="); + const opcode = opcodeList[opcodeList.length - 1]; + + const [copyAlertVis, setCopyAlertVis] = useState(false); - const [copyAlertVis, setCopyAlertVis] = useState(false); + const copyText = function(textToCopy){ + navigator.clipboard.writeText(textToCopy).then(() => { + setCopyAlertvis(true); + }) + } - const copyText = function (textToCopy) { - navigator.clipboard.writeText(textToCopy).then(() => { - setCopyAlertvis(true); - }); - }; + let copyButton; + if (window.cordova.platformId == "ios"){ + copyButton = {copyText(opcode); setCopyAlertVis(true)}} style={styles.button}/> + } - let copyButton; - if (window.cordova.platformId == 'ios') { - copyButton = ( - { - copyText(opcode); - setCopyAlertVis(true); - }} - style={styles.button} - /> - ); - } + return ( + <> + setVis(false)} + transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t("general-settings.qrcode")} + + {t("general-settings.qrcode-share-title")} + + {opcode} + + + action()} style={styles.button}/> + {copyButton} + + + + - return ( - <> - setVis(false)} transparent={true}> - setVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.qrcode')} - - {t('general-settings.qrcode-share-title')} - - {opcode} - - - action()} style={styles.button} /> - {copyButton} - - - - - - - - ); -}; + + + ) +} const styles = StyleSheet.create({ - title: { - alignItems: 'center', - justifyContent: 'center', - }, - content: { - alignItems: 'center', - justifyContent: 'center', - margin: 5, - }, - button: { - margin: 'auto', - }, - opcode: { - fontFamily: 'monospace', - wordBreak: 'break-word', - marginTop: 5, - }, - text: { - fontWeight: 'bold', - marginBottom: 5, - }, -}); + title: + { + alignItems: 'center', + justifyContent: 'center', + }, + content: { + alignItems: 'center', + justifyContent: 'center', + margin: 5 + }, + button: { + margin: 'auto', + }, + opcode: { + fontFamily: "monospace", + wordBreak: "break-word", + marginTop: 5 + }, + text : { + fontWeight: 'bold', + marginBottom: 5 + } + }); -export default PopOpCode; +export default PopOpCode; \ No newline at end of file diff --git a/www/js/control/PrivacyPolicyModal.tsx b/www/js/control/PrivacyPolicyModal.tsx index 4a85b695f..9a0b06d07 100644 --- a/www/js/control/PrivacyPolicyModal.tsx +++ b/www/js/control/PrivacyPolicyModal.tsx @@ -1,90 +1,75 @@ -import React, { useMemo } from 'react'; -import { Modal, useWindowDimensions, ScrollView, Linking, StyleSheet, Text } from 'react-native'; +import React, { useMemo } from "react"; +import { Modal, useWindowDimensions, ScrollView, Linking, StyleSheet, Text } from "react-native"; import { Dialog, Button, useTheme } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; -import useAppConfig from '../useAppConfig'; -import i18next from 'i18next'; -import { settingStyles } from './ProfileSettings'; +import { useTranslation } from "react-i18next"; +import useAppConfig from "../useAppConfig"; +import i18next from "i18next"; +import { settingStyles } from "./ProfileSettings"; const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis }) => { - const { t } = useTranslation(); - const { height: windowHeight } = useWindowDimensions(); - const { colors } = useTheme(); - const { appConfig, loading } = useAppConfig(); + const { t } = useTranslation(); + const { height: windowHeight } = useWindowDimensions(); + const { colors } = useTheme(); + const { appConfig, loading } = useAppConfig(); + + const getTemplateText = function(configObject) { + if (configObject && (configObject.name)) { + return configObject.intro.translated_text[i18next.language]; + } + } - const getTemplateText = function (configObject) { - if (configObject && configObject.name) { - return configObject.intro.translated_text[i18next.language]; + let opCodeText; + if(appConfig?.opcode?.autogen) { + opCodeText = {t('consent-text.opcode.autogen')}; + + } else { + opCodeText = {t('consent-text.opcode.not-autogen')}; } - }; - let opCodeText; - if (appConfig?.opcode?.autogen) { - opCodeText = {t('consent-text.opcode.autogen')}; - } else { - opCodeText = {t('consent-text.opcode.not-autogen')}; - } + let yourRightsText; + if(appConfig?.intro?.app_required) { + yourRightsText = {t('consent-text.rights.app-required', {program_admin_contact: appConfig?.intro?.program_admin_contact})}; - let yourRightsText; - if (appConfig?.intro?.app_required) { - yourRightsText = ( - - {t('consent-text.rights.app-required', { - program_admin_contact: appConfig?.intro?.program_admin_contact, - })} - - ); - } else { - yourRightsText = ( - - {t('consent-text.rights.app-not-required', { - program_or_study: appConfig?.intro?.program_or_study, - })} - - ); - } + } else { + yourRightsText = {t('consent-text.rights.app-not-required', {program_or_study: appConfig?.intro?.program_or_study})}; + } - const templateText = useMemo(() => getTemplateText(appConfig), [appConfig]); + const templateText = useMemo(() => getTemplateText(appConfig), [appConfig]); - return ( - <> - setPrivacyVis(false)} transparent={true}> - setPrivacyVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('consent-text.title')} - - - {t('consent-text.introduction.header')} - {templateText?.short_textual_description} - {'\n'} - {t('consent-text.introduction.what-is-openpath')} - {'\n'} - - {t('consent-text.introduction.what-is-NREL', { - program_or_study: appConfig?.intro?.program_or_study, - })} - - {'\n'} - {t('consent-text.introduction.if-disagree')} - {'\n'} + return ( + <> + setPrivacyVis(false)} transparent={true}> + setPrivacyVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('consent-text.title')} + + + {t('consent-text.introduction.header')} + {templateText?.short_textual_description} + {'\n'} + {t('consent-text.introduction.what-is-openpath')} + {'\n'} + {t('consent-text.introduction.what-is-NREL', {program_or_study: appConfig?.intro?.program_or_study})} + {'\n'} + {t('consent-text.introduction.if-disagree')} + {'\n'} - {t('consent-text.why.header')} - {templateText?.why_we_collect} - {'\n'} + {t('consent-text.why.header')} + {templateText?.why_we_collect} + {'\n'} - {t('consent-text.what.header')} - {t('consent-text.what.no-pii')} - {'\n'} - {t('consent-text.what.phone-sensor')} - {'\n'} - {t('consent-text.what.labeling')} - {'\n'} - {t('consent-text.what.demographics')} - {'\n'} - {t('consent-text.what.on-nrel-site')} - {/* Linking is broken, look into enabling after migration + {t('consent-text.what.header')} + {t('consent-text.what.no-pii')} + {'\n'} + {t('consent-text.what.phone-sensor')} + {'\n'} + {t('consent-text.what.labeling')} + {'\n'} + {t('consent-text.what.demographics')} + {'\n'} + {t('consent-text.what.on-nrel-site')} + {/* Linking is broken, look into enabling after migration {t('consent-text.what.open-source-data')} { {' '}https://github.com/e-mission/em-public-dashboard.git{' '} */} - {'\n'} + {'\n'} - {t('consent-text.opcode.header')} - {opCodeText} - {'\n'} + {t('consent-text.opcode.header')} + {opCodeText} + {'\n'} - {t('consent-text.who-sees.header')} - {t('consent-text.who-sees.public-dash')} - {'\n'} - {t('consent-text.who-sees.individual-info')} - {'\n'} - - {t('consent-text.who-sees.program-admins', { - deployment_partner_name: appConfig?.intro?.deployment_partner_name, - raw_data_use: templateText?.raw_data_use, - })} - - {t('consent-text.who-sees.nrel-devs')} - {'\n'} - - {t('consent-text.who-sees.TSDC-info')} - {/* Linking is broken, look into enabling after migration + {t('consent-text.who-sees.header')} + {t('consent-text.who-sees.public-dash')} + {'\n'} + {t('consent-text.who-sees.individual-info')} + {'\n'} + {t('consent-text.who-sees.program-admins', { + deployment_partner_name: appConfig?.intro?.deployment_partner_name, + raw_data_use: templateText?.raw_data_use})} + {t('consent-text.who-sees.nrel-devs')} + {'\n'} + {t('consent-text.who-sees.TSDC-info')} + {/* Linking is broken, look into enabling after migration { @@ -156,16 +137,15 @@ const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis }) => { }}> {t('consent-text.who-sees.fact-sheet')} */} - {t('consent-text.who-sees.on-nrel-site')} - - {'\n'} + {t('consent-text.who-sees.on-nrel-site')} + + {'\n'} - {t('consent-text.rights.header')} - {yourRightsText} - {'\n'} - - {t('consent-text.rights.destroy-data-pt1')} - {/* Linking is broken, look into enabling after migration + {t('consent-text.rights.header')} + {yourRightsText} + {'\n'} + {t('consent-text.rights.destroy-data-pt1')} + {/* Linking is broken, look into enabling after migration { @@ -173,47 +153,44 @@ const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis }) => { }}> k.shankari@nrel.gov */} - (k.shankari@nrel.gov) - {t('consent-text.rights.destroy-data-pt2')} - - {'\n'} + (k.shankari@nrel.gov) + {t('consent-text.rights.destroy-data-pt2')} + + {'\n'} + + {t('consent-text.questions.header')} + {t('consent-text.questions.for-questions', {program_admin_contact: appConfig?.intro?.program_admin_contact})} + {'\n'} + + {t('consent-text.consent.header')} + {t('consent-text.consent.press-button-to-consent', {program_or_study: appConfig?.intro?.program_or_study})} - {t('consent-text.questions.header')} - - {t('consent-text.questions.for-questions', { - program_admin_contact: appConfig?.intro?.program_admin_contact, - })} - - {'\n'} + + + + + + + - {t('consent-text.consent.header')} - - {t('consent-text.consent.press-button-to-consent', { - program_or_study: appConfig?.intro?.program_or_study, - })} - - - - - - - - - - ); -}; + + ) +} const styles = StyleSheet.create({ - hyperlinkStyle: (linkColor) => ({ - color: linkColor, - }), - text: { - fontSize: 14, - }, - header: { - fontWeight: 'bold', - fontSize: 18, - }, -}); + hyperlinkStyle: (linkColor) => ({ + color: linkColor + }), + text: { + fontSize: 14 + }, + header: { + fontWeight: "bold", + fontSize: 18 + } + }); -export default PrivacyPolicyModal; +export default PrivacyPolicyModal; \ No newline at end of file diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 80ce360f9..dcbedbd78 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -1,698 +1,541 @@ -import React, { useState, useEffect } from 'react'; -import { Modal, StyleSheet, ScrollView } from 'react-native'; -import { Dialog, Button, useTheme, Text, Appbar, IconButton } 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'; -import AlertBar from './AlertBar'; -import DataDatePicker from './DataDatePicker'; -import AppStatusModal from './AppStatusModal'; -import PrivacyPolicyModal from './PrivacyPolicyModal'; -import ActionMenu from '../components/ActionMenu'; -import SensedPage from './SensedPage'; -import LogPage from './LogPage'; -import ControlSyncHelper, { ForceSyncRow, getHelperSyncSettings } from './ControlSyncHelper'; -import ControlCollectionHelper, { - getHelperCollectionSettings, - getState, - isMediumAccuracy, - helperToggleLowAccuracy, - forceTransition, -} from './ControlCollectionHelper'; +import React, { useState, useEffect } from "react"; +import { Modal, StyleSheet, ScrollView } from "react-native"; +import { Dialog, Button, useTheme, Text, Appbar, IconButton } 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"; +import AlertBar from "./AlertBar"; +import DataDatePicker from "./DataDatePicker"; +import AppStatusModal from "./AppStatusModal"; +import PrivacyPolicyModal from "./PrivacyPolicyModal"; +import ActionMenu from "../components/ActionMenu"; +import SensedPage from "./SensedPage" +import LogPage from "./LogPage"; +import ControlSyncHelper, {ForceSyncRow, getHelperSyncSettings} from "./ControlSyncHelper"; +import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransition} from "./ControlCollectionHelper"; //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(); - - //angular services needed - const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); - const UploadHelper = getAngularService('UploadHelper'); - const EmailHelper = getAngularService('EmailHelper'); - const KVStore = getAngularService('KVStore'); - const NotificationScheduler = getAngularService('NotificationScheduler'); - const ControlHelper = getAngularService('ControlHelper'); - const ClientStats = getAngularService('ClientStats'); - const StartPrefs = getAngularService('StartPrefs'); - const DynamicConfig = getAngularService('DynamicConfig'); - - //functions that come directly from an Angular service - const editCollectionConfig = () => setEditCollection(true); - const editSyncConfig = () => setEditSync(true); - - //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 [permitVis, setPermitVis] = useState(false); - const [logoutVis, setLogoutVis] = useState(false); - const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); - const [noConsentVis, setNoConsentVis] = useState(false); - const [noConsentMessageVis, setNoConsentMessageVis] = useState(false); - const [consentVis, setConsentVis] = useState(false); - const [dateDumpVis, setDateDumpVis] = useState(false); - const [privacyVis, setPrivacyVis] = useState(false); - const [showingSensed, setShowingSensed] = useState(false); - const [showingLog, setShowingLog] = useState(false); - const [editSync, setEditSync] = useState(false); - const [editCollection, setEditCollection] = useState(false); - - // const [collectConfig, setCollectConfig] = useState({}); - const [collectSettings, setCollectSettings] = useState({}); - const [notificationSettings, setNotificationSettings] = useState({}); - const [authSettings, setAuthSettings] = useState({}); - const [syncSettings, setSyncSettings] = useState({}); - const [cacheResult, setCacheResult] = useState(''); - const [connectSettings, setConnectSettings] = useState({}); - const [appVersion, setAppVersion] = useState(''); - const [uiConfig, setUiConfig] = useState({}); - const [consentDoc, setConsentDoc] = useState({}); - const [dumpDate, setDumpDate] = useState(new Date()); - - 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(() => { - //added appConfig.name needed to be defined because appConfig was defined but empty - if (appConfig && appConfig.name) { - whenReady(appConfig); + // anything that mutates must go in --- depend on props or state... + const { t } = useTranslation(); + const { appConfig, loading } = useAppConfig(); + const { colors } = useTheme(); + + //angular services needed + const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); + const UploadHelper = getAngularService('UploadHelper'); + const EmailHelper = getAngularService('EmailHelper'); + const KVStore = getAngularService('KVStore'); + const NotificationScheduler = getAngularService('NotificationScheduler'); + const ControlHelper = getAngularService('ControlHelper'); + const ClientStats = getAngularService('ClientStats'); + const StartPrefs = getAngularService('StartPrefs'); + const DynamicConfig = getAngularService('DynamicConfig'); + + //functions that come directly from an Angular service + const editCollectionConfig = () => setEditCollection(true); + const editSyncConfig = () => setEditSync(true); + + //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 [permitVis, setPermitVis] = useState(false); + const [logoutVis, setLogoutVis] = useState(false); + const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); + const [noConsentVis, setNoConsentVis] = useState(false); + const [noConsentMessageVis, setNoConsentMessageVis] = useState(false); + const [consentVis, setConsentVis] = useState(false); + const [dateDumpVis, setDateDumpVis] = useState(false); + const [privacyVis, setPrivacyVis] = useState(false); + const [showingSensed, setShowingSensed] = useState(false); + const [showingLog, setShowingLog] = useState(false); + const [editSync, setEditSync] = useState(false); + const [editCollection, setEditCollection] = useState(false); + + // const [collectConfig, setCollectConfig] = useState({}); + const [collectSettings, setCollectSettings] = useState({}); + const [notificationSettings, setNotificationSettings] = useState({}); + const [authSettings, setAuthSettings] = useState({}); + const [syncSettings, setSyncSettings] = useState({}); + const [cacheResult, setCacheResult] = useState(""); + const [connectSettings, setConnectSettings] = useState({}); + const [appVersion, setAppVersion] = useState(""); + const [uiConfig, setUiConfig] = useState({}); + const [consentDoc, setConsentDoc] = useState({}); + const [dumpDate, setDumpDate] = useState(new Date()); + + 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(() => { + //added appConfig.name needed to be defined because appConfig was defined but empty + if (appConfig && (appConfig.name)) { + whenReady(appConfig); + } + }, [appConfig]); + + const refreshScreen = function() { + refreshCollectSettings(); + refreshNotificationSettings(); + getOPCode(); + getSyncSettings(); + getConnectURL(); + setAppVersion(ClientStats.getAppVersion()); } - }, [appConfig]); - - const refreshScreen = function () { - refreshCollectSettings(); - refreshNotificationSettings(); - getOPCode(); - getSyncSettings(); - getConnectURL(); - setAppVersion(ClientStats.getAppVersion()); - }; - - //previously not loaded on regular refresh, this ensures it stays caught up - useEffect(() => { - refreshNotificationSettings(); - }, [uiConfig]); - - const whenReady = function (newAppConfig) { - var tempUiConfig = newAppConfig; - - // backwards compat hack to fill in the raw_data_use for programs that don't have it - const default_raw_data_use = { - en: `to monitor the ${tempUiConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, - es: `para monitorear el ${tempUiConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes`, - }; - Object.entries(tempUiConfig.intro.translated_text).forEach(([lang, val]) => { - val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; - }); - - // Backwards compat hack to fill in the `app_required` based on the - // old-style "program_or_study" - // remove this at the end of 2023 when all programs have been migrated over - if (tempUiConfig.intro.app_required == undefined) { - tempUiConfig.intro.app_required = tempUiConfig?.intro.program_or_study == 'program'; + + //previously not loaded on regular refresh, this ensures it stays caught up + useEffect(() => { + refreshNotificationSettings(); + }, [uiConfig]) + + const whenReady = function(newAppConfig){ + var tempUiConfig = newAppConfig; + + // backwards compat hack to fill in the raw_data_use for programs that don't have it + const default_raw_data_use = { + "en": `to monitor the ${tempUiConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, + "es": `para monitorear el ${tempUiConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes` + } + Object.entries(tempUiConfig.intro.translated_text).forEach(([lang, val]) => { + val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; + }); + + // Backwards compat hack to fill in the `app_required` based on the + // old-style "program_or_study" + // remove this at the end of 2023 when all programs have been migrated over + if (tempUiConfig.intro.app_required == undefined) { + tempUiConfig.intro.app_required = tempUiConfig?.intro.program_or_study == 'program'; + } + tempUiConfig.opcode = tempUiConfig.opcode || {}; + if (tempUiConfig.opcode.autogen == undefined) { + tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; + } + + // setTemplateText(tempUiConfig.intro.translated_text); + // console.log("translated text is??", templateText); + setUiConfig(tempUiConfig); + refreshScreen(); } - tempUiConfig.opcode = tempUiConfig.opcode || {}; - if (tempUiConfig.opcode.autogen == undefined) { - tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; + + async function refreshCollectSettings() { + console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); + const newCollectSettings = {}; + + // // refresh collect plugin configuration + const collectionPluginConfig = await getHelperCollectionSettings(); + newCollectSettings.config = collectionPluginConfig; + + const collectionPluginState = await getState(); + newCollectSettings.state = collectionPluginState; + newCollectSettings.trackingOn = collectionPluginState != "local.state.tracking_stopped" + && collectionPluginState != "STATE_TRACKING_STOPPED"; + + const isLowAccuracy = await isMediumAccuracy(); + if (typeof isLowAccuracy != 'undefined') { + newCollectSettings.lowAccuracy = isLowAccuracy; + } + + setCollectSettings(newCollectSettings); } - // setTemplateText(tempUiConfig.intro.translated_text); - // console.log("translated text is??", templateText); - setUiConfig(tempUiConfig); - refreshScreen(); - }; - - async function refreshCollectSettings() { - console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); - const newCollectSettings = {}; - - // // refresh collect plugin configuration - const collectionPluginConfig = await getHelperCollectionSettings(); - newCollectSettings.config = collectionPluginConfig; - - const collectionPluginState = await getState(); - newCollectSettings.state = collectionPluginState; - newCollectSettings.trackingOn = - collectionPluginState != 'local.state.tracking_stopped' && - collectionPluginState != 'STATE_TRACKING_STOPPED'; - - const isLowAccuracy = await isMediumAccuracy(); - if (typeof isLowAccuracy != 'undefined') { - newCollectSettings.lowAccuracy = isLowAccuracy; + //ensure ui table updated when editor closes + useEffect(() => { + refreshCollectSettings(); + }, [editCollection]) + + async function refreshNotificationSettings() { + console.debug('about to refreshNotificationSettings, notificationSettings = ', notificationSettings); + const newNotificationSettings ={}; + + if (uiConfig?.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); } - setCollectSettings(newCollectSettings); - } + async function getSyncSettings() { + console.log("getting sync settings"); + var newSyncSettings = {}; + getHelperSyncSettings().then(function(showConfig) { + newSyncSettings.show_config = showConfig; + setSyncSettings(newSyncSettings); + console.log("sync settings are ", syncSettings); + }); + }; + + //update sync settings in the table when close editor + useEffect(() => { + getSyncSettings(); + }, [editSync]); + + async function getConnectURL() { + ControlHelper.getSettings().then(function(response) { + var newConnectSettings ={} + newConnectSettings.url = response.connectUrl; + console.log(response); + setConnectSettings(newConnectSettings); + }, function(error) { + Logger.displayError("While getting connect url", error); + }); + } + + async function getOPCode() { + const newAuthSettings = {}; + const opcode = await ControlHelper.getOPCode(); + if(opcode == null){ + newAuthSettings.opcode = "Not logged in"; + } else { + newAuthSettings.opcode = opcode; + } + setAuthSettings(newAuthSettings); + }; + + //methods that control the settings + const uploadLog = function () { + UploadHelper.uploadFile("loggerDB") + }; - //ensure ui table updated when editor closes - useEffect(() => { - refreshCollectSettings(); - }, [editCollection]); + const emailLog = function () { + // Passing true, we want to send logs + EmailHelper.sendEmail("loggerDB") + }; - async function refreshNotificationSettings() { - console.debug( - 'about to refreshNotificationSettings, notificationSettings = ', - notificationSettings, - ); - const newNotificationSettings = {}; - - if (uiConfig?.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); + 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(); + }); + } } - console.log( - 'notification settings before and after', - notificationSettings, - newNotificationSettings, - ); - setNotificationSettings(newNotificationSettings); - } - - async function getSyncSettings() { - console.log('getting sync settings'); - var newSyncSettings = {}; - getHelperSyncSettings().then(function (showConfig) { - newSyncSettings.show_config = showConfig; - setSyncSettings(newSyncSettings); - console.log('sync settings are ', syncSettings); - }); - } - - //update sync settings in the table when close editor - useEffect(() => { - getSyncSettings(); - }, [editSync]); - - async function getConnectURL() { - ControlHelper.getSettings().then( - function (response) { - var newConnectSettings = {}; - newConnectSettings.url = response.connectUrl; - console.log(response); - setConnectSettings(newConnectSettings); - }, - function (error) { - Logger.displayError('While getting connect url', error); - }, - ); - } - - async function getOPCode() { - const newAuthSettings = {}; - const opcode = await ControlHelper.getOPCode(); - if (opcode == null) { - newAuthSettings.opcode = 'Not logged in'; - } else { - newAuthSettings.opcode = opcode; + function dummyNotification() { + cordova.plugins.notification.local.addActions('dummy-actions', [ + { id: 'action', title: 'Yes' }, + { id: 'cancel', title: 'No' } + ]); + cordova.plugins.notification.local.schedule({ + id: new Date().getTime(), + title: 'Dummy Title', + text: 'Dummy text', + actions: 'dummy-actions', + trigger: {at: new Date(new Date().getTime() + 5000)}, + }); } - setAuthSettings(newAuthSettings); - } - - //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'; + forceTransition(transitionToForce); + refreshCollectSettings(); } - } - - function dummyNotification() { - cordova.plugins.notification.local.addActions('dummy-actions', [ - { id: 'action', title: 'Yes' }, - { id: 'cancel', title: 'No' }, - ]); - cordova.plugins.notification.local.schedule({ - id: new Date().getTime(), - title: 'Dummy Title', - text: 'Dummy text', - actions: 'dummy-actions', - trigger: { at: new Date(new Date().getTime() + 5000) }, - }); - } - - async function userStartStopTracking() { - const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; - forceTransition(transitionToForce); - refreshCollectSettings(); - } - - async function toggleLowAccuracy() { - let toggle = await helperToggleLowAccuracy(); - refreshCollectSettings(); - } - - const shareQR = function () { - /*code adapted from demo of react-qr-code*/ - const svg = document.querySelector('.qr-code'); - const svgData = new XMLSerializer().serializeToString(svg); - const img = new Image(); - - img.onload = () => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - const pngFile = canvas.toDataURL('image/png'); - - var prepopulateQRMessage = {}; - prepopulateQRMessage.files = [pngFile]; - prepopulateQRMessage.url = authSettings.opcode; - prepopulateQRMessage.message = authSettings.opcode; //text saved to files with image! - - 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); - }, - ); - }; - img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; - }; - - const viewQRCode = function (e) { - setOpCodeVis(true); - }; - - const clearNotifications = function () { - window.cordova.plugins.notification.local.clearAll(); - }; - - //Platform.OS returns "web" now, but could be used once it's fully a Native app - //for now, use window.cordova.platformId - - const parseState = function (state) { - console.log('state in parse state is', state); - if (state) { - console.log('state in parse state exists', window.cordova.platformId); - if (window.cordova.platformId == 'android') { - console.log('ANDROID state in parse state is', state.substring(12)); - return state.substring(12); - } else if (window.cordova.platformId == 'ios') { - console.log('IOS state in parse state is', state.substring(6)); - return state.substring(6); - } + + async function toggleLowAccuracy() { + let toggle = await helperToggleLowAccuracy(); + refreshCollectSettings(); } - }; - - async function invalidateCache() { - window.cordova.plugins.BEMUserCache.invalidateAllCache().then( - function (result) { - console.log('invalidate result', result); - setCacheResult(result); - setInvalidateSuccessVis(true); - }, - function (error) { - Logger.displayError('while invalidating cache, error->', error); - }, - ); - } - - //in ProfileSettings in DevZone (above two functions are helpers) - async function checkConsent() { - StartPrefs.getConsentDocument().then( - function (resultDoc) { - setConsentDoc(resultDoc); - if (resultDoc == null) { - setNoConsentVis(true); - } else { - setConsentVis(true); + + const shareQR = function() { + /*code adapted from demo of react-qr-code*/ + const svg = document.querySelector(".qr-code"); + const svgData = new XMLSerializer().serializeToString(svg); + const img = new Image(); + + img.onload = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const pngFile = canvas.toDataURL("image/png"); + + var prepopulateQRMessage = {}; + prepopulateQRMessage.files = [pngFile]; + prepopulateQRMessage.url = authSettings.opcode; + prepopulateQRMessage.message = authSettings.opcode; //text saved to files with image! + + 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); + }); } - }, - function (error) { - Logger.displayError('Error reading consent document from cache', error); - }, - ); - } - - const onSelectState = function (stateObject) { - forceTransition(stateObject.transition); - }; - - const onSelectCarbon = function (carbonObject) { - console.log('changeCarbonDataset(): chose locale ' + carbonObject.value); - CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here - //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 - carbonDatasetString = - i18next.t('general-settings.carbon-dataset') + - ': ' + - CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - }; - - //conditional creation of setting sections - - 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('')}> - - + img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; + } + + const viewQRCode = function(e) { + setOpCodeVis(true); + } + + const clearNotifications = function() { + window.cordova.plugins.notification.local.clearAll(); + } + + //Platform.OS returns "web" now, but could be used once it's fully a Native app + //for now, use window.cordova.platformId + + const parseState = function(state) { + console.log("state in parse state is", state); + if (state) { + console.log("state in parse state exists", window.cordova.platformId); + if(window.cordova.platformId == 'android') { + console.log("ANDROID state in parse state is", state.substring(12)); + return state.substring(12); + } + else if(window.cordova.platformId == 'ios') { + console.log("IOS state in parse state is", state.substring(6)); + return state.substring(6); + } + } + } + + async function invalidateCache() { + window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { + console.log("invalidate result", result); + setCacheResult(result); + setInvalidateSuccessVis(true); + }, function(error) { + Logger.displayError("while invalidating cache, error->", error); + }); + } + + //in ProfileSettings in DevZone (above two functions are helpers) + async function checkConsent() { + StartPrefs.getConsentDocument().then(function(resultDoc){ + setConsentDoc(resultDoc); + if (resultDoc == null) { + setNoConsentVis(true); + } else { + setConsentVis(true); + } + }, function(error) { + Logger.displayError("Error reading consent document from cache", error) + }); + } + + const onSelectState = function(stateObject) { + forceTransition(stateObject.transition); + } + + const onSelectCarbon = function(carbonObject) { + console.log("changeCarbonDataset(): chose locale " + carbonObject.value); + CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here + //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 + carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + } + + //conditional creation of setting sections + + 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 ( + <> + + + {t('control.log-out')} + setLogoutVis(true)}> + + + + + + setPrivacyVis(true)}> + {timePicker} + + setPermitVis(true)}> + + setCarbonDataVis(true)}> + setDateDumpVis(true)}> + {logUploadSection} + + + + + + + + {notifSchedule} + + setNukeVis(true)}> + setForceStateVis(true)}> + setShowingLog(true)}> + setShowingSensed(true)}> + + + + + + console.log("")} desc={appVersion}> + + + {/* menu for "nuke data" */} + setNukeVis(false)} + transparent={true}> + setNukeVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.clear-data')} + + + + + + + + + + + + {/* menu for "set carbon dataset - only somewhat working" */} + clearNotifications()}> + + {/* force state sheet */} + {}}> + + {/* opcode viewing popup */} + + + {/* {view permissions} */} + + + {/* {view privacy} */} + + + {/* logout menu */} + setLogoutVis(false)} transparent={true}> + setLogoutVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.are-you-sure')} + + {t('general-settings.log-out-warning')} + + + + + + + + + {/* handle no consent */} + setNoConsentVis(false)} transparent={true}> + setNoConsentVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.consent-not-found')} + + + + + + + + {/* handle consent */} + setConsentVis(false)} transparent={true}> + setConsentVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.consented-to', {protocol_id: consentDoc.protocol_id, approval_date: consentDoc.approval_date})} + + + + + + + + + + + + + + + + + + + ); - } - - return ( - <> - - - {t('control.log-out')} - setLogoutVis(true)}> - - - - - - setPrivacyVis(true)}> - {timePicker} - - setPermitVis(true)}> - - setCarbonDataVis(true)}> - setDateDumpVis(true)}> - {logUploadSection} - - - - - - - - {notifSchedule} - - setNukeVis(true)}> - setForceStateVis(true)}> - setShowingLog(true)}> - setShowingSensed(true)}> - - - - - - console.log('')} - desc={appVersion}> - - - {/* menu for "nuke data" */} - setNukeVis(false)} transparent={true}> - setNukeVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.clear-data')} - - - - - - - - - - - - {/* menu for "set carbon dataset - only somewhat working" */} - clearNotifications()}> - - {/* force state sheet */} - {}}> - - {/* opcode viewing popup */} - - - {/* {view permissions} */} - - - {/* {view privacy} */} - - - {/* logout menu */} - setLogoutVis(false)} transparent={true}> - setLogoutVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.are-you-sure')} - - {t('general-settings.log-out-warning')} - - - - - - - - - {/* handle no consent */} - setNoConsentVis(false)} transparent={true}> - setNoConsentVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.consent-not-found')} - - - - - - - - {/* handle consent */} - setConsentVis(false)} transparent={true}> - setConsentVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - - {t('general-settings.consented-to', { - protocol_id: consentDoc.protocol_id, - approval_date: consentDoc.approval_date, - })} - - - - - - - - - - - - - - - - - - - ); }; export const settingStyles = StyleSheet.create({ - dialog: (surfaceColor) => ({ - backgroundColor: surfaceColor, - margin: 5, - marginLeft: 25, - marginRight: 25, - }), - monoDesc: { - fontSize: 12, - fontFamily: 'monospace', - }, -}); - -angularize(ProfileSettings, 'ProfileSettings', 'emission.main.control.profileSettings'); -export default ProfileSettings; + dialog: (surfaceColor) => ({ + backgroundColor: surfaceColor, + margin: 5, + marginLeft: 25, + marginRight: 25 + }), + 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 index b603758b0..40e8485ee 100644 --- a/www/js/control/ReminderTime.tsx +++ b/www/js/control/ReminderTime.tsx @@ -1,70 +1,69 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; import { Modal, StyleSheet } from 'react-native'; import { List, useTheme } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; +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], - ); + const onDismiss = React.useCallback(() => { + setVisible(false) + }, [setVisible]) - return ( - setVisible(false)} transparent={true}> - - - ); -}; + 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); + const { t } = useTranslation(); + const { colors } = useTheme(); + const [pickTimeVis, setPickTimeVis] = useState(false); - let rightComponent = ; + let rightComponent = ; - return ( - <> - + setPickTimeVis(true)} right={() => rightComponent} - /> + /> + + - - - ); + + ); }; const styles = StyleSheet.create({ - item: (surfaceColor) => ({ - justifyContent: 'space-between', - alignContent: 'center', - backgroundColor: surfaceColor, - margin: 1, - }), -}); + item: (surfaceColor) => ({ + justifyContent: 'space-between', + alignContent: 'center', + backgroundColor: surfaceColor, + margin: 1, + }), + }); -export default ReminderTime; +export default ReminderTime; \ No newline at end of file diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index 5e5fce18c..2f62d5c6c 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -1,101 +1,91 @@ -import React, { useState, useEffect } from 'react'; -import { View, StyleSheet, SafeAreaView, Modal } from 'react-native'; -import { useTheme, Appbar, IconButton, Text } from 'react-native-paper'; -import { getAngularService } from '../angular-react-helper'; -import { useTranslation } from 'react-i18next'; +import React, { useState, useEffect } from "react"; +import { View, StyleSheet, SafeAreaView, Modal } from "react-native"; +import { useTheme, Appbar, IconButton, Text } from "react-native-paper"; +import { getAngularService } from "../angular-react-helper"; +import { useTranslation } from "react-i18next"; import { FlashList } from '@shopify/flash-list'; -import moment from 'moment'; +import moment from "moment"; -const SensedPage = ({ pageVis, setPageVis }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const EmailHelper = getAngularService('EmailHelper'); +const SensedPage = ({pageVis, setPageVis}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const EmailHelper = getAngularService('EmailHelper'); - /* Let's keep a reference to the database for convenience */ - const [DB, setDB] = useState(); - const [entries, setEntries] = useState([]); + /* Let's keep a reference to the database for convenience */ + const [ DB, setDB ]= useState(); + const [ entries, setEntries ] = useState([]); - const emailCache = function () { - EmailHelper.sendEmail('userCacheDB'); - }; - - async function updateEntries() { - //hardcoded function and keys after eliminating bit-rotted options - setDB(window.cordova.plugins.BEMUserCache); - let userCacheFn = DB.getAllMessages; - let userCacheKey = 'statemachine/transition'; - try { - let entryList = await userCacheFn(userCacheKey, true); - let tempEntries = []; - entryList.forEach((entry) => { - entry.metadata.write_fmt_time = moment - .unix(entry.metadata.write_ts) - .tz(entry.metadata.time_zone) - .format('llll'); - entry.data = JSON.stringify(entry.data, null, 2); - tempEntries.push(entry); - }); - setEntries(tempEntries); - } catch (error) { - window.Logger.log(window.Logger.LEVEL_ERROR, 'Error updating entries' + error); + const emailCache = function() { + EmailHelper.sendEmail("userCacheDB"); } - } - useEffect(() => { - updateEntries(); - }, [pageVis]); + async function updateEntries() { + //hardcoded function and keys after eliminating bit-rotted options + setDB(window.cordova.plugins.BEMUserCache); + let userCacheFn = DB.getAllMessages; + let userCacheKey = "statemachine/transition"; + try { + let entryList = await userCacheFn(userCacheKey, true); + let tempEntries = []; + entryList.forEach(entry => { + entry.metadata.write_fmt_time = moment.unix(entry.metadata.write_ts) + .tz(entry.metadata.time_zone) + .format("llll"); + entry.data = JSON.stringify(entry.data, null, 2); + tempEntries.push(entry); + }); + setEntries(tempEntries); + } + catch(error) { + window.Logger.log(window.Logger.LEVEL_ERROR, "Error updating entries"+ error); + } + } - const separator = () => ; - const cacheItem = ({ item: cacheItem }) => ( - - - {cacheItem.metadata.write_fmt_time} - - - {cacheItem.data} - - - ); + useEffect(() => { + updateEntries(); + }, [pageVis]); - return ( - setPageVis(false)}> - - - setPageVis(false)} /> - - + const separator = () => + const cacheItem = ({item: cacheItem}) => ( + {cacheItem.metadata.write_fmt_time} + {cacheItem.data} + ); - - updateEntries()} /> - emailCache()} /> - + return ( + setPageVis(false)}> + + + setPageVis(false)}/> + + - item.metadata.write_ts} - ItemSeparatorComponent={separator} - /> - - - ); + + updateEntries()}/> + emailCache()}/> + + + item.metadata.write_ts} + ItemSeparatorComponent={separator} + /> + + + ); }; const styles = StyleSheet.create({ - date: (surfaceColor) => ({ - backgroundColor: surfaceColor, - }), - details: { - fontFamily: 'monospace', - }, - entry: (surfaceColor) => ({ - backgroundColor: surfaceColor, - marginLeft: 5, - }), -}); + date: (surfaceColor) => ({ + backgroundColor: surfaceColor, + }), + details: { + fontFamily: "monospace", + }, + entry: (surfaceColor) => ({ + backgroundColor: surfaceColor, + marginLeft: 5, + }), + }); -export default SensedPage; +export default SensedPage; \ No newline at end of file diff --git a/www/js/control/SettingRow.jsx b/www/js/control/SettingRow.jsx index b55b3c804..473a45d7f 100644 --- a/www/js/control/SettingRow.jsx +++ b/www/js/control/SettingRow.jsx @@ -1,59 +1,52 @@ -import React from 'react'; +import React from "react"; import { StyleSheet } from 'react-native'; import { List, Switch, useTheme } from 'react-native-paper'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; -const SettingRow = ({ - textKey, - iconName = undefined, - action, - desc = undefined, - switchValue = undefined, - descStyle = undefined, -}) => { - const { t } = useTranslation(); //this accesses the translations - const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors +const SettingRow = ({textKey, iconName=undefined, action, desc=undefined, switchValue=undefined, 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 = ''; - } + let rightComponent; + if (iconName) { + rightComponent = ; + } else { + rightComponent = ; + } + let descriptionText; + if(desc) { + descriptionText = {desc}; + } else { + descriptionText = ""; + } - return ( - action(e)} - right={() => rightComponent} - /> - ); + 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, - }, -}); + 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/emailService.js b/www/js/control/emailService.js index 8eeaf39bb..0374adf5a 100644 --- a/www/js/control/emailService.js +++ b/www/js/control/emailService.js @@ -2,113 +2,96 @@ import angular from 'angular'; -angular - .module('emission.services.email', ['emission.plugin.logger']) +angular.module('emission.services.email', ['emission.plugin.logger']) - .service('EmailHelper', function ($window, $http, Logger) { - const getEmailConfig = function () { - return new Promise(function (resolve, reject) { - window.Logger.log(window.Logger.LEVEL_INFO, 'About to get email config'); - var address = []; - $http - .get('json/emailConfig.json') - .then(function (emailConfig) { - window.Logger.log( - window.Logger.LEVEL_DEBUG, - 'emailConfigString = ' + JSON.stringify(emailConfig.data), - ); - address.push(emailConfig.data.address); - resolve(address); - }) - .catch(function (err) { - $http - .get('json/emailConfig.json.sample') - .then(function (emailConfig) { - window.Logger.log( - window.Logger.LEVEL_DEBUG, - 'default emailConfigString = ' + JSON.stringify(emailConfig.data), - ); - address.push(emailConfig.data.address); - resolve(address); - }) - .catch(function (err) { - window.Logger.log( - window.Logger.LEVEL_ERROR, - 'Error while reading default email config' + err, - ); - reject(err); - }); - }); - }); - }; + .service('EmailHelper', function ($window, $http, Logger) { - const hasAccount = function () { - return new Promise(function (resolve, reject) { - $window.cordova.plugins.email.hasAccount(function (hasAct) { - resolve(hasAct); - }); - }); - }; + const getEmailConfig = function () { + return new Promise(function (resolve, reject) { + window.Logger.log(window.Logger.LEVEL_INFO, "About to get email config"); + var address = []; + $http.get("json/emailConfig.json").then(function (emailConfig) { + window.Logger.log(window.Logger.LEVEL_DEBUG, "emailConfigString = " + JSON.stringify(emailConfig.data)); + address.push(emailConfig.data.address) + resolve(address); + }).catch(function (err) { + $http.get("json/emailConfig.json.sample").then(function (emailConfig) { + window.Logger.log(window.Logger.LEVEL_DEBUG, "default emailConfigString = " + JSON.stringify(emailConfig.data)); + address.push(emailConfig.data.address) + resolve(address); + }).catch(function (err) { + window.Logger.log(window.Logger.LEVEL_ERROR, "Error while reading default email config" + err); + reject(err); + }); + }); + }); + } - this.sendEmail = function (database) { - Promise.all([getEmailConfig(), hasAccount()]).then(function ([address, hasAct]) { - var parentDir = 'unknown'; + const hasAccount = function() { + return new Promise(function(resolve, reject) { + $window.cordova.plugins.email.hasAccount(function (hasAct) { + resolve(hasAct); + }); + }); + } - // Check this only for ios, since for android, the check always fails unless - // the user grants the "GET_ACCOUNTS" dynamic permission - // without the permission, we only see the e-mission account which is not valid - // - // https://developer.android.com/reference/android/accounts/AccountManager#getAccounts() - // - // Caller targeting API level below Build.VERSION_CODES.O that - // have not been granted the Manifest.permission.GET_ACCOUNTS - // permission, will only see those accounts managed by - // AbstractAccountAuthenticators whose signature matches the - // client. - // and on android, if the account is not configured, the gmail app will be launched anyway - // on iOS, nothing will happen. So we perform the check only on iOS so that we can - // generate a reasonably relevant error message + this.sendEmail = function (database) { + Promise.all([getEmailConfig(), hasAccount()]).then(function([address, hasAct]) { + var parentDir = "unknown"; - if (ionic.Platform.isIOS() && !hasAct) { - alert(i18next.t('email-service.email-account-not-configured')); - return; - } + // Check this only for ios, since for android, the check always fails unless + // the user grants the "GET_ACCOUNTS" dynamic permission + // without the permission, we only see the e-mission account which is not valid + // + // https://developer.android.com/reference/android/accounts/AccountManager#getAccounts() + // + // Caller targeting API level below Build.VERSION_CODES.O that + // have not been granted the Manifest.permission.GET_ACCOUNTS + // permission, will only see those accounts managed by + // AbstractAccountAuthenticators whose signature matches the + // client. + // and on android, if the account is not configured, the gmail app will be launched anyway + // on iOS, nothing will happen. So we perform the check only on iOS so that we can + // generate a reasonably relevant error message - if (ionic.Platform.isAndroid()) { - parentDir = 'app://databases'; - } - if (ionic.Platform.isIOS()) { - alert(i18next.t('email-service.email-account-mail-app')); - parentDir = cordova.file.dataDirectory + '../LocalDatabase'; - } + if (ionic.Platform.isIOS() && !hasAct) { + alert(i18next.t('email-service.email-account-not-configured')); + return; + } - if (parentDir == 'unknown') { - alert('parentDir unexpectedly = ' + parentDir + '!'); - } + if (ionic.Platform.isAndroid()) { + parentDir = "app://databases"; + } + if (ionic.Platform.isIOS()) { + alert(i18next.t('email-service.email-account-mail-app')); + parentDir = cordova.file.dataDirectory + "../LocalDatabase"; + } + + if (parentDir == "unknown") { + alert("parentDir unexpectedly = " + parentDir + "!") + } - window.Logger.log(window.Logger.LEVEL_INFO, 'Going to email ' + database); - parentDir = parentDir + '/' + database; - /* + window.Logger.log(window.Logger.LEVEL_INFO, "Going to email " + database); + parentDir = parentDir + "/" + database; + /* window.Logger.log(window.Logger.LEVEL_INFO, "Going to export logs to "+parentDir); */ - alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); - var email = { - to: address, - attachments: [parentDir], - subject: i18next.t('email-service.email-log.subject-logs'), - body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong'), - }; + alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); + var email = { + to: address, + attachments: [ + parentDir + ], + subject: i18next.t('email-service.email-log.subject-logs'), + body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong') + } - $window.cordova.plugins.email.open(email, function () { - Logger.log( - 'email app closed while sending, ' + - JSON.stringify(email) + - ' not sure if we should do anything', - ); - // alert(i18next.t('email-service.no-email-address-configured') + err); - return; - }); - }); - }; - }); + $window.cordova.plugins.email.open(email, function () { + Logger.log("email app closed while sending, "+JSON.stringify(email)+" not sure if we should do anything"); + // alert(i18next.t('email-service.no-email-address-configured') + err); + return; + }); + }); + }; +}); diff --git a/www/js/control/general-settings.js b/www/js/control/general-settings.js index 44842cdab..e29702d6e 100644 --- a/www/js/control/general-settings.js +++ b/www/js/control/general-settings.js @@ -3,53 +3,34 @@ import angular from 'angular'; import ProfileSettings from './ProfileSettings'; -angular - .module('emission.main.control', [ - 'emission.services', - 'emission.i18n.utils', - 'emission.main.control.collection', - 'emission.main.control.sync', - 'emission.splash.localnotify', - 'emission.splash.notifscheduler', - 'emission.splash.startprefs', - 'emission.main.metrics.factory', - 'emission.stats.clientstats', - 'emission.plugin.kvstore', - 'emission.plugin.logger', - 'emission.config.dynamic', - ProfileSettings.module, - ]) +angular.module('emission.main.control',['emission.services', + 'emission.i18n.utils', + 'emission.main.control.collection', + 'emission.main.control.sync', + 'emission.splash.localnotify', + 'emission.splash.notifscheduler', + 'emission.splash.startprefs', + 'emission.main.metrics.factory', + 'emission.stats.clientstats', + 'emission.plugin.kvstore', + 'emission.plugin.logger', + 'emission.config.dynamic', + ProfileSettings.module]) - .controller( - 'ControlCtrl', - function ( - $scope, - $ionicPlatform, - $state, - $ionicPopover, - i18nUtils, - $ionicModal, - $stateParams, - Logger, - KVStore, - CalorieCal, - ClientStats, - StartPrefs, - ControlHelper, - EmailHelper, - UploadHelper, - ControlCollectionHelper, - ControlSyncHelper, - CarbonDatasetHelper, - NotificationScheduler, - DynamicConfig, - ) { - console.log('controller ControlCtrl called without params'); +.controller('ControlCtrl', function($scope, $ionicPlatform, + $state, $ionicPopover, i18nUtils, + $ionicModal, $stateParams, Logger, + KVStore, CalorieCal, ClientStats, + StartPrefs, ControlHelper, EmailHelper, UploadHelper, + ControlCollectionHelper, ControlSyncHelper, + CarbonDatasetHelper, NotificationScheduler, DynamicConfig) { - //used to have on "afterEnter" that checked for 2 things - //modal launch -> migrated into AppStatusModal w/ use of custom hook! - //stateParams.openTimeOfDayPicker -> functionality lost for now - //to change reminder time if accessing profile by specific android notification flow - //would open the date picker - }, - ); + console.log("controller ControlCtrl called without params"); + + //used to have on "afterEnter" that checked for 2 things + //modal launch -> migrated into AppStatusModal w/ use of custom hook! + //stateParams.openTimeOfDayPicker -> functionality lost for now + //to change reminder time if accessing profile by specific android notification flow + //would open the date picker + +}); diff --git a/www/js/control/uploadService.js b/www/js/control/uploadService.js index 4d4d51a45..6f95503c1 100644 --- a/www/js/control/uploadService.js +++ b/www/js/control/uploadService.js @@ -2,199 +2,170 @@ import angular from 'angular'; -angular - .module('emission.services.upload', ['emission.plugin.logger']) - - .service('UploadHelper', function ($window, $http, $rootScope, $ionicPopup, Logger) { - const getUploadConfig = function () { - return new Promise(function (resolve, reject) { - Logger.log(Logger.LEVEL_INFO, 'About to get email config'); - var url = []; - $http - .get('json/uploadConfig.json') - .then(function (uploadConfig) { - Logger.log( - Logger.LEVEL_DEBUG, - 'uploadConfigString = ' + JSON.stringify(uploadConfig.data), - ); - url.push(uploadConfig.data.url); - resolve(url); - }) - .catch(function (err) { - $http - .get('json/uploadConfig.json.sample') - .then(function (uploadConfig) { - Logger.log( - Logger.LEVEL_DEBUG, - 'default uploadConfigString = ' + JSON.stringify(uploadConfig.data), - ); - url.push(uploadConfig.data.url); - resolve(url); - }) - .catch(function (err) { - Logger.log(Logger.LEVEL_ERROR, 'Error while reading default upload config' + err); - reject(err); - }); - }); - }); - }; - - const onReadError = function (err) { - Logger.displayError('Error while reading log', err); - }; - - const onUploadError = function (err) { - Logger.displayError('Error while uploading log', err); - }; - - const readDBFile = function (parentDir, database, callbackFn) { - return new Promise(function (resolve, reject) { - window.resolveLocalFileSystemURL(parentDir, function (fs) { - fs.filesystem.root.getFile( - fs.fullPath + database, - null, - (fileEntry) => { - console.log(fileEntry); - fileEntry.file(function (file) { - console.log(file); - var reader = new FileReader(); - - reader.onprogress = function (report) { - console.log('Current progress is ' + JSON.stringify(report)); - if (callbackFn != undefined) { - callbackFn((report.loaded * 100) / report.total); - } - }; - - reader.onerror = function (error) { - console.log(this.error); - reject({ error: { message: this.error } }); - }; - - reader.onload = function () { - console.log( - 'Successful file read with ' + this.result.byteLength + ' characters', - ); - resolve(new DataView(this.result)); - }; - - reader.readAsArrayBuffer(file); - }, reject); - }, - reject, - ); - }); - }); - }; - - const sendToServer = function upload(url, binArray, params) { - var config = { - headers: { 'Content-Type': undefined }, - transformRequest: angular.identity, - params: params, - }; - return $http.post(url, binArray, config); - }; - - this.uploadFile = function (database) { - getUploadConfig() - .then((uploadConfig) => { - var parentDir = 'unknown'; - - if (ionic.Platform.isAndroid()) { - parentDir = cordova.file.applicationStorageDirectory + '/databases'; - } - if (ionic.Platform.isIOS()) { - parentDir = cordova.file.dataDirectory + '../LocalDatabase'; - } - - if (parentDir === 'unknown') { - alert('parentDir unexpectedly = ' + parentDir + '!'); - } - - const newScope = $rootScope.$new(); - newScope.data = {}; - newScope.fromDirText = i18next.t('upload-service.upload-from-dir', { - parentDir: parentDir, - }); - newScope.toServerText = i18next.t('upload-service.upload-to-server', { - serverURL: uploadConfig, - }); - - var didCancel = true; - - const detailsPopup = $ionicPopup.show({ - title: i18next.t('upload-service.upload-database', { db: database }), - template: - newScope.toServerText + - '', - scope: newScope, - buttons: [ - { - text: 'Cancel', - onTap: function (e) { - didCancel = true; - detailsPopup.close(); - }, - }, - { - text: 'Upload', - type: 'button-positive', - onTap: function (e) { - if (!newScope.data.reason) { - //don't allow the user to close unless he enters wifi password - didCancel = false; - e.preventDefault(); - } else { - didCancel = false; - return newScope.data.reason; +angular.module('emission.services.upload', ['emission.plugin.logger']) + + .service('UploadHelper', function ($window, $http, $rootScope, $ionicPopup, Logger) { + const getUploadConfig = function () { + return new Promise(function (resolve, reject) { + Logger.log(Logger.LEVEL_INFO, "About to get email config"); + var url = []; + $http.get("json/uploadConfig.json").then(function (uploadConfig) { + Logger.log(Logger.LEVEL_DEBUG, "uploadConfigString = " + JSON.stringify(uploadConfig.data)); + url.push(uploadConfig.data.url) + resolve(url); + }).catch(function (err) { + $http.get("json/uploadConfig.json.sample").then(function (uploadConfig) { + Logger.log(Logger.LEVEL_DEBUG, "default uploadConfigString = " + JSON.stringify(uploadConfig.data)); + url.push(uploadConfig.data.url) + resolve(url); + }).catch(function (err) { + Logger.log(Logger.LEVEL_ERROR, "Error while reading default upload config" + err); + reject(err); + }); + }); + }); + } + + const onReadError = function(err) { + Logger.displayError("Error while reading log", err); + } + + const onUploadError = function(err) { + Logger.displayError("Error while uploading log", err); + } + + const readDBFile = function(parentDir, database, callbackFn) { + return new Promise(function(resolve, reject) { + window.resolveLocalFileSystemURL(parentDir, function(fs) { + fs.filesystem.root.getFile(fs.fullPath+database, null, (fileEntry) => { + console.log(fileEntry); + fileEntry.file(function(file) { + console.log(file); + var reader = new FileReader(); + + reader.onprogress = function(report) { + console.log("Current progress is "+JSON.stringify(report)); + if (callbackFn != undefined) { + callbackFn(report.loaded * 100 / report.total); + } + } + + reader.onerror = function(error) { + console.log(this.error); + reject({"error": {"message": this.error}}); + } + + reader.onload = function() { + console.log("Successful file read with " + this.result.byteLength +" characters"); + resolve(new DataView(this.result)); + } + + reader.readAsArrayBuffer(file); + }, reject); + }, reject); + }); + }); + } + + const sendToServer = function upload(url, binArray, params) { + var config = { + headers: {'Content-Type': undefined }, + transformRequest: angular.identity, + params: params + }; + return $http.post(url, binArray, config); + } + + this.uploadFile = function (database) { + getUploadConfig().then((uploadConfig) => { + var parentDir = "unknown"; + + if (ionic.Platform.isAndroid()) { + parentDir = cordova.file.applicationStorageDirectory+"/databases"; + } + if (ionic.Platform.isIOS()) { + parentDir = cordova.file.dataDirectory + "../LocalDatabase"; + } + + if (parentDir === "unknown") { + alert("parentDir unexpectedly = " + parentDir + "!") + } + + const newScope = $rootScope.$new(); + newScope.data = {}; + newScope.fromDirText = i18next.t('upload-service.upload-from-dir', {parentDir: parentDir}); + newScope.toServerText = i18next.t('upload-service.upload-to-server', {serverURL: uploadConfig}); + + var didCancel = true; + + const detailsPopup = $ionicPopup.show({ + title: i18next.t("upload-service.upload-database", { db: database }), + template: newScope.toServerText + + '', + scope: newScope, + buttons: [ + { + text: 'Cancel', + onTap: function(e) { + didCancel = true; + detailsPopup.close(); + } + }, + { + text: 'Upload', + type: 'button-positive', + onTap: function(e) { + if (!newScope.data.reason) { + //don't allow the user to close unless he enters wifi password + didCancel = false; + e.preventDefault(); + } else { + didCancel = false; + return newScope.data.reason; + } + } } - }, - }, - ], - }); - - Logger.log(Logger.LEVEL_INFO, 'Going to upload ' + database); - const readFileAndInfo = [readDBFile(parentDir, database), detailsPopup]; - Promise.all(readFileAndInfo) - .then(([binString, reason]) => { - if (!didCancel) { - console.log('Uploading file of size ' + binString.byteLength); + ] + }); + + Logger.log(Logger.LEVEL_INFO, "Going to upload " + database); + const readFileAndInfo = [readDBFile(parentDir, database), detailsPopup]; + Promise.all(readFileAndInfo).then(([binString, reason]) => { + if(!didCancel) + { + console.log("Uploading file of size "+binString.byteLength); const progressScope = $rootScope.$new(); const params = { - reason: reason, - tz: Intl.DateTimeFormat().resolvedOptions().timeZone, - }; + reason: reason, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone + } uploadConfig.forEach((url) => { - const progressPopup = $ionicPopup.show({ - title: i18next.t('upload-service.upload-database', { db: database }), - template: - i18next.t('upload-service.upload-progress', { - filesizemb: binString.byteLength / (1000 * 1000), - serverURL: uploadConfig, - }) + '
', - scope: progressScope, - buttons: [{ text: 'Cancel', type: 'button-cancel' }], - }); - sendToServer(url, binString, params) - .then((response) => { - console.log(response); - progressPopup.close(); - const successPopup = $ionicPopup.alert({ - title: i18next.t('upload-service.upload-success'), - template: i18next.t('upload-service.upload-details', { - filesizemb: binString.byteLength / (1000 * 1000), - serverURL: uploadConfig, - }), - }); - }) - .catch(onUploadError); + const progressPopup = $ionicPopup.show({ + title: i18next.t("upload-service.upload-database", + {db: database}), + template: i18next.t("upload-service.upload-progress", + {filesizemb: binString.byteLength / (1000 * 1000), + serverURL: uploadConfig}) + + '
', + scope: progressScope, + buttons: [ + { text: 'Cancel', type: 'button-cancel', }, + ] + }); + sendToServer(url, binString, params).then((response) => { + console.log(response); + progressPopup.close(); + const successPopup = $ionicPopup.alert({ + title: i18next.t("upload-service.upload-success"), + template: i18next.t("upload-service.upload-details", + {filesizemb: binString.byteLength / (1000 * 1000), + serverURL: uploadConfig}) + }); + }).catch(onUploadError); }); } - }) - .catch(onReadError); - }) - .catch(onReadError); - }; - }); + }).catch(onReadError); + }).catch(onReadError); + }; +}); diff --git a/www/js/controllers.js b/www/js/controllers.js index 0a2c348e7..7efc26c09 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -2,134 +2,90 @@ import angular from 'angular'; -angular - .module('emission.controllers', [ - 'emission.splash.startprefs', - 'emission.splash.pushnotify', - 'emission.splash.storedevicesettings', - 'emission.splash.localnotify', - 'emission.splash.remotenotify', - 'emission.stats.clientstats', - ]) +angular.module('emission.controllers', ['emission.splash.startprefs', + 'emission.splash.pushnotify', + 'emission.splash.storedevicesettings', + 'emission.splash.localnotify', + 'emission.splash.remotenotify', + 'emission.stats.clientstats']) - .controller('RootCtrl', function ($scope) {}) +.controller('RootCtrl', function($scope) {}) - .controller('DashCtrl', function ($scope) {}) +.controller('DashCtrl', function($scope) {}) - .controller( - 'SplashCtrl', - function ( - $scope, - $state, - $interval, - $rootScope, - StartPrefs, - PushNotify, - StoreDeviceSettings, - LocalNotify, - RemoteNotify, - ClientStats, - ) { - console.log('SplashCtrl invoked'); - // alert("attach debugger!"); - // PushNotify.startupInit(); +.controller('SplashCtrl', function($scope, $state, $interval, $rootScope, + StartPrefs, PushNotify, StoreDeviceSettings, + LocalNotify, RemoteNotify, ClientStats) { + console.log('SplashCtrl invoked'); + // alert("attach debugger!"); + // PushNotify.startupInit(); - $rootScope.$on( - '$stateChangeSuccess', - function (event, toState, toParams, fromState, fromParams) { - console.log( - 'Finished changing state from ' + - JSON.stringify(fromState) + - ' to ' + - JSON.stringify(toState), - ); - ClientStats.addReading( - ClientStats.getStatKeys().STATE_CHANGED, - fromState.name + '-2-' + toState.name, - ).then( - function () {}, - function () {}, - ); - }, - ); - $rootScope.$on( - '$stateChangeError', - function (event, toState, toParams, fromState, fromParams, error) { - console.log( - 'Error ' + - error + - ' while changing state from ' + - JSON.stringify(fromState) + - ' to ' + - JSON.stringify(toState), - ); - ClientStats.addError( - ClientStats.getStatKeys().STATE_CHANGED, - fromState.name + '-2-' + toState.name + '_' + error, - ).then( - function () {}, - function () {}, - ); - }, - ); - $rootScope.$on('$stateNotFound', function (event, unfoundState, fromState, fromParams) { - console.log('unfoundState.to = ' + unfoundState.to); // "lazy.state" - console.log('unfoundState.toParams = ' + unfoundState.toParams); // {a:1, b:2} - console.log('unfoundState.options = ' + unfoundState.options); // {inherit:false} + default options - ClientStats.addError( - ClientStats.getStatKeys().STATE_CHANGED, - fromState.name + '-2-' + unfoundState.name, - ).then( - function () {}, - function () {}, - ); - }); + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){ + console.log("Finished changing state from "+JSON.stringify(fromState) + + " to "+JSON.stringify(toState)); + ClientStats.addReading(ClientStats.getStatKeys().STATE_CHANGED, + fromState.name + '-2-' + toState.name).then(function() {}, function() {}); + }); + $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){ + console.log("Error "+error+" while changing state from "+JSON.stringify(fromState) + +" to "+JSON.stringify(toState)); + ClientStats.addError(ClientStats.getStatKeys().STATE_CHANGED, + fromState.name + '-2-' + toState.name+ "_" + error).then(function() {}, function() {}); + }); + $rootScope.$on('$stateNotFound', + function(event, unfoundState, fromState, fromParams){ + console.log("unfoundState.to = "+unfoundState.to); // "lazy.state" + console.log("unfoundState.toParams = " + unfoundState.toParams); // {a:1, b:2} + console.log("unfoundState.options = " + unfoundState.options); // {inherit:false} + default options + ClientStats.addError(ClientStats.getStatKeys().STATE_CHANGED, + fromState.name + '-2-' + unfoundState.name).then(function() {}, function() {}); + }); + + var isInList = function(element, list) { + return list.indexOf(element) != -1 + } - var isInList = function (element, list) { - return list.indexOf(element) != -1; - }; + $rootScope.$on('$stateChangeStart', + function(event, toState, toParams, fromState, fromParams, options){ + var personalTabs = ['root.main.common.map', + 'root.main.control', + 'root.main.metrics'] + if (isInList(toState.name, personalTabs)) { + // toState is in the personalTabs list + StartPrefs.getPendingOnboardingState().then(function(result) { + if (result != null) { + event.preventDefault(); + $state.go(result); + }; + // else, will do default behavior, which is to go to the tab + }); + } + }) + console.log('SplashCtrl invoke finished'); +}) - $rootScope.$on( - '$stateChangeStart', - function (event, toState, toParams, fromState, fromParams, options) { - var personalTabs = ['root.main.common.map', 'root.main.control', 'root.main.metrics']; - if (isInList(toState.name, personalTabs)) { - // toState is in the personalTabs list - StartPrefs.getPendingOnboardingState().then(function (result) { - if (result != null) { - event.preventDefault(); - $state.go(result); - } - // else, will do default behavior, which is to go to the tab - }); - } - }, - ); - 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) { - //}); +.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); - }; - }) + $scope.chats = Chats.all(); + $scope.remove = function(chat) { + Chats.remove(chat); + }; +}) - .controller('ChatDetailCtrl', function ($scope, $stateParams, Chats) { - $scope.chat = Chats.get($stateParams.chatId); - }) +.controller('ChatDetailCtrl', function($scope, $stateParams, Chats) { + $scope.chat = Chats.get($stateParams.chatId); +}) - .controller('AccountCtrl', function ($scope) { - $scope.settings = { - enableFriends: true, - }; - }); +.controller('AccountCtrl', function($scope) { + $scope.settings = { + enableFriends: true + }; +}); diff --git a/www/js/diary.js b/www/js/diary.js index e83477f56..8fb257ad8 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -1,25 +1,23 @@ import angular from 'angular'; import LabelTab from './diary/LabelTab'; -angular - .module('emission.main.diary', [ - 'emission.main.diary.services', - 'emission.survey.external.launch', - 'emission.survey.multilabel.buttons', - 'emission.survey.multilabel.infscrollfilters', - 'emission.survey.enketo.add-note-button', - 'emission.survey.enketo.trip.infscrollfilters', - 'emission.plugin.logger', - LabelTab.module, - ]) +angular.module('emission.main.diary',['emission.main.diary.services', + 'emission.survey.external.launch', + 'emission.survey.multilabel.buttons', + 'emission.survey.multilabel.infscrollfilters', + 'emission.survey.enketo.add-note-button', + 'emission.survey.enketo.trip.infscrollfilters', + 'emission.plugin.logger', + LabelTab.module]) - .config(function ($stateProvider) { - $stateProvider.state('root.main.inf_scroll', { - url: '/inf_scroll', +.config(function($stateProvider) { + $stateProvider + .state('root.main.inf_scroll', { + url: "/inf_scroll", views: { 'main-inf-scroll': { - template: '', + template: "", }, - }, - }); - }); + } + }) +}); diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 15f87d003..36c24ada2 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -6,28 +6,23 @@ share the data that has been loaded and interacted with. */ -import React, { useEffect, useState, useRef } from 'react'; -import { angularize, getAngularService } from '../angular-react-helper'; -import useAppConfig from '../useAppConfig'; -import { useTranslation } from 'react-i18next'; -import { invalidateMaps } from '../components/LeafletView'; -import moment from 'moment'; -import LabelListScreen from './list/LabelListScreen'; -import { createStackNavigator } from '@react-navigation/stack'; -import LabelScreenDetails from './details/LabelDetailsScreen'; -import { NavigationContainer } from '@react-navigation/native'; -import { - compositeTrips2TimelineMap, - getAllUnprocessedInputs, - getLocalUnprocessedInputs, - populateCompositeTrips, -} from './timelineHelper'; -import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHelper'; -import { SurveyOptions } from '../survey/survey'; -import { getLabelOptions } from '../survey/multilabel/confirmHelper'; -import { displayError } from '../plugin/logger'; -import AppStatusModal from '../control/AppStatusModal'; -import { useTheme } from 'react-native-paper'; +import React, { useEffect, useState, useRef } from "react"; +import { angularize, getAngularService } from "../angular-react-helper"; +import useAppConfig from "../useAppConfig"; +import { useTranslation } from "react-i18next"; +import { invalidateMaps } from "../components/LeafletView"; +import moment from "moment"; +import LabelListScreen from "./list/LabelListScreen"; +import { createStackNavigator } from "@react-navigation/stack"; +import LabelScreenDetails from "./details/LabelDetailsScreen"; +import { NavigationContainer } from "@react-navigation/native"; +import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips } from "./timelineHelper"; +import { fillLocationNamesOfTrip, resetNominatimLimiter } from "./addressNamesHelper"; +import { SurveyOptions } from "../survey/survey"; +import { getLabelOptions } from "../survey/multilabel/confirmHelper"; +import { displayError } from "../plugin/logger"; +import AppStatusModal from "../control/AppStatusModal"; +import { useTheme } from "react-native-paper"; let labelPopulateFactory, labelsResultMap, notesResultMap, showPlaces; const ONE_DAY = 24 * 60 * 60; // seconds @@ -47,7 +42,7 @@ const LabelTab = () => { const [timelineMap, setTimelineMap] = useState(null); const [displayedEntries, setDisplayedEntries] = useState(null); const [refreshTime, setRefreshTime] = useState(null); - const [isLoading, setIsLoading] = useState('replace'); + const [isLoading, setIsLoading] = useState('replace'); const $rootScope = getAngularService('$rootScope'); const $state = getAngularService('$state'); @@ -80,8 +75,7 @@ const LabelTab = () => { // initalize filters const tripFilterFactory = getAngularService(surveyOpt.filter); const allFalseFilters = tripFilterFactory.configuredFilters.map((f, i) => ({ - ...f, - state: i == 0 ? true : false, // only the first filter will have state true on init + ...f, state: (i == 0 ? true : false) // only the first filter will have state true on init })); setFilterInputs(allFalseFilters); } @@ -97,7 +91,7 @@ const LabelTab = () => { let entriesToDisplay = allEntries; if (activeFilter) { const entriesAfterFilter = allEntries.filter( - (t) => t.justRepopulated || activeFilter?.filter(t), + t => t.justRepopulated || activeFilter?.filter(t) ); /* next, filter out any untracked time if the trips that came before and after it are no longer displayed */ @@ -117,20 +111,12 @@ const LabelTab = () => { async function loadTimelineEntries() { try { const pipelineRange = await CommHelper.getPipelineRangeTs(); - [labelsResultMap, notesResultMap] = await getAllUnprocessedInputs( - pipelineRange, - labelPopulateFactory, - enbs, - ); - Logger.log( - 'After reading unprocessedInputs, labelsResultMap =' + - JSON.stringify(labelsResultMap) + - '; notesResultMap = ' + - JSON.stringify(notesResultMap), - ); + [labelsResultMap, notesResultMap] = await getAllUnprocessedInputs(pipelineRange, labelPopulateFactory, enbs); + Logger.log("After reading unprocessedInputs, labelsResultMap =" + JSON.stringify(labelsResultMap) + + "; notesResultMap = " + JSON.stringify(notesResultMap)); setPipelineRange(pipelineRange); } catch (error) { - Logger.displayError('Error while loading pipeline range', error); + Logger.displayError("Error while loading pipeline range", error); setIsLoading(false); } } @@ -150,39 +136,34 @@ const LabelTab = () => { setRefreshTime(new Date()); } - async function loadAnotherWeek(when: 'past' | 'future') { + async function loadAnotherWeek(when: 'past'|'future') { try { - const reachedPipelineStart = - queriedRange?.start_ts && queriedRange.start_ts <= pipelineRange.start_ts; - const reachedPipelineEnd = - queriedRange?.end_ts && queriedRange.end_ts >= pipelineRange.end_ts; + const reachedPipelineStart = queriedRange?.start_ts && queriedRange.start_ts <= pipelineRange.start_ts; + const reachedPipelineEnd = queriedRange?.end_ts && queriedRange.end_ts >= pipelineRange.end_ts; if (!queriedRange) { // first time loading - if (!isLoading) setIsLoading('replace'); + if(!isLoading) setIsLoading('replace'); const nowTs = new Date().getTime() / 1000; const [ctList, utList] = await fetchTripsInRange(pipelineRange.end_ts - ONE_WEEK, nowTs); handleFetchedTrips(ctList, utList, 'replace'); - setQueriedRange({ start_ts: pipelineRange.end_ts - ONE_WEEK, end_ts: nowTs }); + setQueriedRange({start_ts: pipelineRange.end_ts - ONE_WEEK, end_ts: nowTs}); } else if (when == 'past' && !reachedPipelineStart) { - if (!isLoading) setIsLoading('prepend'); + if(!isLoading) setIsLoading('prepend'); const fetchStartTs = Math.max(queriedRange.start_ts - ONE_WEEK, pipelineRange.start_ts); - const [ctList, utList] = await fetchTripsInRange( - queriedRange.start_ts - ONE_WEEK, - queriedRange.start_ts - 1, - ); + const [ctList, utList] = await fetchTripsInRange(queriedRange.start_ts - ONE_WEEK, queriedRange.start_ts - 1); handleFetchedTrips(ctList, utList, 'prepend'); - setQueriedRange({ start_ts: fetchStartTs, end_ts: queriedRange.end_ts }); + setQueriedRange({start_ts: fetchStartTs, end_ts: queriedRange.end_ts}) } else if (when == 'future' && !reachedPipelineEnd) { - if (!isLoading) setIsLoading('append'); + if(!isLoading) setIsLoading('append'); const fetchEndTs = Math.min(queriedRange.end_ts + ONE_WEEK, pipelineRange.end_ts); const [ctList, utList] = await fetchTripsInRange(queriedRange.end_ts + 1, fetchEndTs); handleFetchedTrips(ctList, utList, 'append'); - setQueriedRange({ start_ts: queriedRange.start_ts, end_ts: fetchEndTs }); + setQueriedRange({start_ts: queriedRange.start_ts, end_ts: fetchEndTs}) } } catch (e) { setIsLoading(false); - displayError(e, t('errors.while-loading-another-week', { when: when })); + displayError(e, t('errors.while-loading-another-week', {when: when})); } } @@ -194,30 +175,20 @@ const LabelTab = () => { const threeDaysAfter = moment(day).add(3, 'days').unix(); const [ctList, utList] = await fetchTripsInRange(threeDaysBefore, threeDaysAfter); handleFetchedTrips(ctList, utList, 'replace'); - setQueriedRange({ start_ts: threeDaysBefore, end_ts: threeDaysAfter }); + setQueriedRange({start_ts: threeDaysBefore, end_ts: threeDaysAfter}); } catch (e) { setIsLoading(false); - displayError(e, t('errors.while-loading-specific-week', { day: day })); + displayError(e, t('errors.while-loading-specific-week', {day: day})); } } function handleFetchedTrips(ctList, utList, mode: 'prepend' | 'append' | 'replace') { const tripsRead = ctList.concat(utList); - populateCompositeTrips( - tripsRead, - showPlaces, - labelPopulateFactory, - labelsResultMap, - enbs, - notesResultMap, - ); + populateCompositeTrips(tripsRead, showPlaces, labelPopulateFactory, labelsResultMap, enbs, notesResultMap); // Fill place names on a reversed copy of the list so we fill from the bottom up - tripsRead - .slice() - .reverse() - .forEach(function (trip, index) { - fillLocationNamesOfTrip(trip); - }); + tripsRead.slice().reverse().forEach(function (trip, index) { + fillLocationNamesOfTrip(trip); + }); const readTimelineMap = compositeTrips2TimelineMap(tripsRead, showPlaces); if (mode == 'append') { setTimelineMap(new Map([...timelineMap, ...readTimelineMap])); @@ -226,13 +197,13 @@ const LabelTab = () => { } else if (mode == 'replace') { setTimelineMap(readTimelineMap); } else { - return console.error('Unknown insertion mode ' + mode); + return console.error("Unknown insertion mode " + mode); } } async function fetchTripsInRange(startTs: number, endTs: number) { if (!pipelineRange.start_ts) { - console.warn('trying to read data too early, early return'); + console.warn("trying to read data too early, early return"); return; } @@ -240,22 +211,16 @@ const LabelTab = () => { let readUnprocessedPromise; if (endTs >= pipelineRange.end_ts) { const nowTs = new Date().getTime() / 1000; - const lastProcessedTrip = - timelineMap && - [...timelineMap?.values()] - .reverse() - .find((trip) => trip.origin_key.includes('confirmed_trip')); - readUnprocessedPromise = Timeline.readUnprocessedTrips( - pipelineRange.end_ts, - nowTs, - lastProcessedTrip, + const lastProcessedTrip = timelineMap && [...timelineMap?.values()].reverse().find( + trip => trip.origin_key.includes('confirmed_trip') ); + readUnprocessedPromise = Timeline.readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); } else { readUnprocessedPromise = Promise.resolve([]); } const results = await Promise.all([readCompositePromise, readUnprocessedPromise]); return results; - } + }; useEffect(() => { if (!displayedEntries) return; @@ -265,7 +230,7 @@ const LabelTab = () => { // TODO move this out of LabelTab; should be a global check & popup that can show in any tab function checkPermissionsStatus() { - $rootScope.$broadcast('recomputeAppStatus', (status) => { + $rootScope.$broadcast("recomputeAppStatus", (status) => { if (!status) { setPermissionVis(true); //if the status is false, popup modal } @@ -274,15 +239,10 @@ const LabelTab = () => { const timelineMapRef = useRef(timelineMap); async function repopulateTimelineEntry(oid: string) { - if (!timelineMap.has(oid)) - return console.error('Item with oid: ' + oid + ' not found in timeline'); - const [newLabels, newNotes] = await getLocalUnprocessedInputs( - pipelineRange, - labelPopulateFactory, - enbs, - ); + if (!timelineMap.has(oid)) return console.error("Item with oid: " + oid + " not found in timeline"); + const [newLabels, newNotes] = await getLocalUnprocessedInputs(pipelineRange, labelPopulateFactory, enbs); const repopTime = new Date().getTime(); - const newEntry = { ...timelineMap.get(oid), justRepopulated: repopTime }; + const newEntry = {...timelineMap.get(oid), justRepopulated: repopTime}; labelPopulateFactory.populateInputsAndInferences(newEntry, newLabels); enbs.populateInputsAndInferences(newEntry, newNotes); const newTimelineMap = new Map(timelineMap).set(oid, newEntry); @@ -293,13 +253,10 @@ const LabelTab = () => { https://legacy.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function */ timelineMapRef.current = newTimelineMap; setTimeout(() => { - const entry = { ...timelineMapRef.current.get(oid) }; + const entry = {...timelineMapRef.current.get(oid)}; if (entry.justRepopulated != repopTime) - return console.log('Entry ' + oid + ' was repopulated again, skipping'); - const newTimelineMap = new Map(timelineMapRef.current).set(oid, { - ...entry, - justRepopulated: false, - }); + return console.log("Entry " + oid + " was repopulated again, skipping"); + const newTimelineMap = new Map(timelineMapRef.current).set(oid, {...entry, justRepopulated: false}); setTimelineMap(newTimelineMap); }, 30000); } @@ -318,33 +275,28 @@ const LabelTab = () => { loadSpecificWeek, refresh, repopulateTimelineEntry, - }; + } const Tab = createStackNavigator(); return ( - + - + options={{detachPreviousScreen: false}} /> - + ); -}; +} angularize(LabelTab, 'LabelTab', 'emission.main.diary.labeltab'); export default LabelTab; diff --git a/www/js/diary/addressNamesHelper.ts b/www/js/diary/addressNamesHelper.ts index f0e17921a..e7f198fbe 100644 --- a/www/js/diary/addressNamesHelper.ts +++ b/www/js/diary/addressNamesHelper.ts @@ -29,7 +29,9 @@ export default function createObserver< }, publish: (entryKey: KeyType, event: EventType) => { if (!listeners[entryKey]) listeners[entryKey] = []; - listeners[entryKey].forEach((listener: Listener) => listener(event)); + listeners[entryKey].forEach((listener: Listener) => + listener(event), + ); }, }; } @@ -39,6 +41,7 @@ export const LocalStorageObserver = createObserver(); export const { subscribe, publish } = LocalStorageObserver; export function useLocalStorage(key: string, initialValue: T) { + const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); @@ -60,7 +63,8 @@ export function useLocalStorage(key: string, initialValue: T) { const setValue = (value: T) => { try { - const valueToStore = value instanceof Function ? value(storedValue) : value; + const valueToStore = + value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); LocalStorageObserver.publish(key, valueToStore); if (typeof window !== 'undefined') { @@ -73,8 +77,11 @@ export function useLocalStorage(key: string, initialValue: T) { return [storedValue, setValue]; } -import Bottleneck from 'bottleneck'; -import { getAngularService } from '../angular-react-helper'; + + + +import Bottleneck from "bottleneck"; +import { getAngularService } from "../angular-react-helper"; let nominatimLimiter = new Bottleneck({ maxConcurrent: 2, minTime: 500 }); export const resetNominatimLimiter = () => { @@ -86,19 +93,15 @@ export const resetNominatimLimiter = () => { // accepts a nominatim response object and returns an address-like string // e.g. "Main St, San Francisco" function toAddressName(data) { - const address = data?.['address']; + const address = data?.["address"]; if (address) { /* Sometimes, the street name ('road') isn't found and is undefined. If so, fallback to 'pedestrian' or 'suburb' or 'neighbourhood' */ - const placeName = - address['road'] || - address['pedestrian'] || - address['suburb'] || - address['neighbourhood'] || - ''; + const placeName = address['road'] || address['pedestrian'] || + address['suburb'] || address['neighbourhood'] || ''; /* This could be either a city or town. If neither, fallback to 'county' */ const municipalityName = address['city'] || address['town'] || address['county'] || ''; - return `${placeName}, ${municipalityName}`; + return `${placeName}, ${municipalityName}` } return '...'; } @@ -112,42 +115,31 @@ async function fetchNominatimLocName(loc_geojson) { const coordsStr = loc_geojson.coordinates.toString(); const cachedResponse = localStorage.getItem(coordsStr); if (cachedResponse) { - console.log( - 'fetchNominatimLocName: found cached response for ', - coordsStr, - cachedResponse, - 'skipping fetch', - ); + console.log('fetchNominatimLocName: found cached response for ', coordsStr, cachedResponse, 'skipping fetch'); return; } - console.log('Getting location name for ', coordsStr); - const url = - 'https://nominatim.openstreetmap.org/reverse?format=json&lat=' + - loc_geojson.coordinates[1] + - '&lon=' + - loc_geojson.coordinates[0]; + console.log("Getting location name for ", coordsStr); + const url = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + loc_geojson.coordinates[1] + "&lon=" + loc_geojson.coordinates[0]; try { const response = await fetch(url); const data = await response.json(); - Logger.log( - `while reading data from nominatim, status = ${response.status} data = ${JSON.stringify( - data, - )}`, - ); + Logger.log(`while reading data from nominatim, status = ${response.status} data = ${JSON.stringify(data)}`); localStorage.setItem(coordsStr, JSON.stringify(data)); publish(coordsStr, data); } catch (error) { if (!nominatimError) { nominatimError = error; - Logger.displayError('while reading address data ', error); + Logger.displayError("while reading address data ", error); } } -} +}; // Schedules nominatim fetches for the start and end locations of a trip export function fillLocationNamesOfTrip(trip) { - nominatimLimiter.schedule(() => fetchNominatimLocName(trip.end_loc)); - nominatimLimiter.schedule(() => fetchNominatimLocName(trip.start_loc)); + nominatimLimiter.schedule(() => + fetchNominatimLocName(trip.end_loc)); + nominatimLimiter.schedule(() => + fetchNominatimLocName(trip.start_loc)); } // a React hook that takes a trip or place and returns an array of its address names diff --git a/www/js/diary/cards/DiaryCard.tsx b/www/js/diary/cards/DiaryCard.tsx index f6e845983..f97a38e46 100644 --- a/www/js/diary/cards/DiaryCard.tsx +++ b/www/js/diary/cards/DiaryCard.tsx @@ -7,53 +7,35 @@ (see appTheme.ts for more info on theme flavors) */ -import React from 'react'; +import React from "react"; import { View, useWindowDimensions, StyleSheet } from 'react-native'; import { Card, PaperProvider, useTheme } from 'react-native-paper'; -import TimestampBadge from './TimestampBadge'; -import useDerivedProperties from '../useDerivedProperties'; +import TimestampBadge from "./TimestampBadge"; +import useDerivedProperties from "../useDerivedProperties"; export const DiaryCard = ({ timelineEntry, children, flavoredTheme, ...otherProps }) => { const { width: windowWidth } = useWindowDimensions(); - const { displayStartTime, displayEndTime, displayStartDateAbbr, displayEndDateAbbr } = - useDerivedProperties(timelineEntry); + const { displayStartTime, displayEndTime, + displayStartDateAbbr, displayEndDateAbbr } = useDerivedProperties(timelineEntry); const theme = flavoredTheme || useTheme(); return ( - - - + + + {children} - - + + ); -}; +} // common styles, used for DiaryCard export const cardStyles = StyleSheet.create({ diff --git a/www/js/diary/cards/ModesIndicator.tsx b/www/js/diary/cards/ModesIndicator.tsx index 37788a789..5211f7ed4 100644 --- a/www/js/diary/cards/ModesIndicator.tsx +++ b/www/js/diary/cards/ModesIndicator.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import { View, StyleSheet } from 'react-native'; -import color from 'color'; +import color from "color"; import { LabelTabContext } from '../LabelTab'; import { logDebug } from '../../plugin/logger'; import { getBaseModeOfLabeledTrip } from '../diaryHelper'; @@ -8,13 +8,14 @@ import { Icon } from '../../components/Icon'; import { Text, useTheme } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; -const ModesIndicator = ({ trip, detectedModes }) => { +const ModesIndicator = ({ trip, detectedModes, }) => { + const { t } = useTranslation(); const { labelOptions } = useContext(LabelTabContext); const { colors } = useTheme(); - const indicatorBackgroundColor = color(colors.onPrimary).alpha(0.8).rgb().string(); - let indicatorBorderColor = color('black').alpha(0.5).rgb().string(); + const indicatorBackgroundColor = color(colors.onPrimary).alpha(.8).rgb().string(); + let indicatorBorderColor = color('black').alpha(.5).rgb().string(); let modeViews; if (trip.userInput.MODE) { @@ -24,56 +25,35 @@ const ModesIndicator = ({ trip, detectedModes }) => { modeViews = ( - + {trip.userInput.MODE.text} ); - } else if ( - detectedModes?.length > 1 || - (detectedModes?.length == 1 && detectedModes[0].mode != 'UNKNOWN') - ) { + } else if (detectedModes?.length > 1 || detectedModes?.length == 1 && detectedModes[0].mode != 'UNKNOWN') { // show detected modes if there are more than one, or if there is only one and it's not UNKNOWN - modeViews = ( - <> - {t('diary.detected')} - {detectedModes?.map?.((pct, i) => ( - - - {/* show percents if there are more than one detected modes */} - {detectedModes?.length > 1 && ( - - {pct.pct}% - - )} - - ))} - - ); + modeViews = (<> + {t('diary.detected')} + {detectedModes?.map?.((pct, i) => ( + + + {/* show percents if there are more than one detected modes */} + {detectedModes?.length > 1 && + {pct.pct}% + } + + ))} + ); } - return ( - modeViews && ( - - - {modeViews} - + return modeViews && ( + + + {modeViews} - ) - ); + + ) }; const s = StyleSheet.create({ diff --git a/www/js/diary/cards/PlaceCard.tsx b/www/js/diary/cards/PlaceCard.tsx index 6c3bc9a9a..481eab4fb 100644 --- a/www/js/diary/cards/PlaceCard.tsx +++ b/www/js/diary/cards/PlaceCard.tsx @@ -6,52 +6,45 @@ PlaceCards use the blueish 'place' theme flavor. */ -import React from 'react'; +import React from "react"; import { View, StyleSheet } from 'react-native'; import { Text } from 'react-native-paper'; -import useAppConfig from '../../useAppConfig'; -import AddNoteButton from '../../survey/enketo/AddNoteButton'; -import AddedNotesList from '../../survey/enketo/AddedNotesList'; -import { getTheme } from '../../appTheme'; -import { DiaryCard, cardStyles } from './DiaryCard'; -import { useAddressNames } from '../addressNamesHelper'; -import useDerivedProperties from '../useDerivedProperties'; -import StartEndLocations from '../components/StartEndLocations'; +import useAppConfig from "../../useAppConfig"; +import AddNoteButton from "../../survey/enketo/AddNoteButton"; +import AddedNotesList from "../../survey/enketo/AddedNotesList"; +import { getTheme } from "../../appTheme"; +import { DiaryCard, cardStyles } from "./DiaryCard"; +import { useAddressNames } from "../addressNamesHelper"; +import useDerivedProperties from "../useDerivedProperties"; +import StartEndLocations from "../components/StartEndLocations"; -type Props = { place: { [key: string]: any } }; +type Props = { place: {[key: string]: any} }; const PlaceCard = ({ place }: Props) => { + const { appConfig, loading } = useAppConfig(); const { displayStartTime, displayEndTime, displayDate } = useDerivedProperties(place); - let [placeDisplayName] = useAddressNames(place); + let [ placeDisplayName ] = useAddressNames(place); const flavoredTheme = getTheme('place'); return ( - - - {/* date and distance */} + + {/* date and distance */} - - {displayDate} - + {displayDate} - - {/* place name */} - + {/* place name */} + - - {/* add note button */} + {/* add note button */} - + storeKey={'manual/place_addition_input'} /> diff --git a/www/js/diary/cards/TimestampBadge.tsx b/www/js/diary/cards/TimestampBadge.tsx index d9262cc58..da753ec76 100644 --- a/www/js/diary/cards/TimestampBadge.tsx +++ b/www/js/diary/cards/TimestampBadge.tsx @@ -1,14 +1,14 @@ /* A presentational component that accepts a time (and optional date) and displays them in a badge Used in the label screen, on the trip, place, and/or untracked cards */ -import React from 'react'; -import { StyleSheet } from 'react-native'; -import { Badge, BadgeProps, Text, useTheme } from 'react-native-paper'; +import React from "react"; +import { StyleSheet } from "react-native"; +import { Badge, BadgeProps, Text, useTheme } from "react-native-paper"; type Props = BadgeProps & { - lightBg: boolean; - time: string; - date?: string; + lightBg: boolean, + time: string, + date?: string, }; const TimestampBadge = ({ lightBg, time, date, ...otherProps }: Props) => { const { colors } = useTheme(); @@ -16,19 +16,17 @@ const TimestampBadge = ({ lightBg, time, date, ...otherProps }: Props) => { const textColor = lightBg ? 'black' : 'white'; return ( - // @ts-ignore Technically, Badge only accepts a string or number as its child, but we want + // @ts-ignore Technically, Badge only accepts a string or number as its child, but we want // to have different bold & light text styles for the time and date, so we pass in Text components. // It works fine with Text components inside, so let's ignore the type error. - - {time} - { - /* if date is not passed as prop, it will not be shown */ - date && ( - - {`\xa0(${date})` /* date shown in parentheses with space before */} - - ) - } + + + {time} + + {/* if date is not passed as prop, it will not be shown */ + date && + {`\xa0(${date})` /* date shown in parentheses with space before */} + } ); }; diff --git a/www/js/diary/cards/TripCard.tsx b/www/js/diary/cards/TripCard.tsx index 2d93264e9..5414b9228 100644 --- a/www/js/diary/cards/TripCard.tsx +++ b/www/js/diary/cards/TripCard.tsx @@ -4,41 +4,35 @@ will used the greyish 'draft' theme flavor. */ -import React, { useContext } from 'react'; +import React, { useContext } from "react"; import { View, useWindowDimensions, StyleSheet } from 'react-native'; import { Text, IconButton } from 'react-native-paper'; -import LeafletView from '../../components/LeafletView'; -import { useTranslation } from 'react-i18next'; -import MultilabelButtonGroup from '../../survey/multilabel/MultiLabelButtonGroup'; -import UserInputButton from '../../survey/enketo/UserInputButton'; -import useAppConfig from '../../useAppConfig'; -import AddNoteButton from '../../survey/enketo/AddNoteButton'; -import AddedNotesList from '../../survey/enketo/AddedNotesList'; -import { getTheme } from '../../appTheme'; -import { DiaryCard, cardStyles } from './DiaryCard'; -import { useNavigation } from '@react-navigation/native'; -import { useAddressNames } from '../addressNamesHelper'; -import { LabelTabContext } from '../LabelTab'; -import useDerivedProperties from '../useDerivedProperties'; -import StartEndLocations from '../components/StartEndLocations'; -import ModesIndicator from './ModesIndicator'; -import { useGeojsonForTrip } from '../timelineHelper'; +import LeafletView from "../../components/LeafletView"; +import { useTranslation } from "react-i18next"; +import MultilabelButtonGroup from "../../survey/multilabel/MultiLabelButtonGroup"; +import UserInputButton from "../../survey/enketo/UserInputButton"; +import useAppConfig from "../../useAppConfig"; +import AddNoteButton from "../../survey/enketo/AddNoteButton"; +import AddedNotesList from "../../survey/enketo/AddedNotesList"; +import { getTheme } from "../../appTheme"; +import { DiaryCard, cardStyles } from "./DiaryCard"; +import { useNavigation } from "@react-navigation/native"; +import { useAddressNames } from "../addressNamesHelper"; +import { LabelTabContext } from "../LabelTab"; +import useDerivedProperties from "../useDerivedProperties"; +import StartEndLocations from "../components/StartEndLocations"; +import ModesIndicator from "./ModesIndicator"; +import { useGeojsonForTrip } from "../timelineHelper"; -type Props = { trip: { [key: string]: any } }; +type Props = { trip: {[key: string]: any}}; const TripCard = ({ trip }: Props) => { + const { t } = useTranslation(); const { width: windowWidth } = useWindowDimensions(); const { appConfig, loading } = useAppConfig(); - const { - displayStartTime, - displayEndTime, - displayDate, - formattedDistance, - distanceSuffix, - displayTime, - detectedModes, - } = useDerivedProperties(trip); - let [tripStartDisplayName, tripEndDisplayName] = useAddressNames(trip); + const { displayStartTime, displayEndTime, displayDate, formattedDistance, + distanceSuffix, displayTime, detectedModes } = useDerivedProperties(trip); + let [ tripStartDisplayName, tripEndDisplayName ] = useAddressNames(trip); const navigation = useNavigation(); const { surveyOpt, labelOptions } = useContext(LabelTabContext); const tripGeojson = useGeojsonForTrip(trip, labelOptions, trip?.userInput?.MODE?.value); @@ -48,7 +42,7 @@ const TripCard = ({ trip }: Props) => { function showDetail() { const tripId = trip._id.$oid; - navigation.navigate('label.details', { tripId, flavoredTheme }); + navigation.navigate("label.details", { tripId, flavoredTheme }); } const mapOpts = { zoomControl: false, dragging: false }; @@ -56,82 +50,52 @@ const TripCard = ({ trip }: Props) => { const mapStyle = showAddNoteButton ? s.shortenedMap : s.fullHeightMap; return ( showDetail()}> - - showDetail()} - style={{ - position: 'absolute', - right: 0, - top: 0, - height: 16, - width: 32, - justifyContent: 'center', - margin: 4, - }} - /> - - {/* right panel */} - - {/* date and distance */} - - - {displayDate} - + showDetail()} + style={{position: 'absolute', right: 0, top: 0, height: 16, width: 32, + justifyContent: 'center', margin: 4}} /> + {/* right panel */} + {/* date and distance */} + + {displayDate} - - {t('diary.distance-in-time', { - distance: formattedDistance, - distsuffix: distanceSuffix, - time: displayTime, - })} + + {t('diary.distance-in-time', {distance: formattedDistance, distsuffix: distanceSuffix, time: displayTime})} - - {/* start and end locations */} - + {/* start and end locations */} + - - {/* mode and purpose buttons / survey button */} - {surveyOpt?.elementTag == 'multilabel' && } - {surveyOpt?.elementTag == 'enketo-trip-button' && ( - - )} + {/* mode and purpose buttons / survey button */} + {surveyOpt?.elementTag == 'multilabel' && + } + {surveyOpt?.elementTag == 'enketo-trip-button' + && } - - {/* left panel */} - {/* left panel */} + + style={[{minHeight: windowWidth / 2}, mapStyle]} /> - {showAddNoteButton && ( + {showAddNoteButton && - + - )} + } - {trip.additionsList?.length != 0 && ( + {trip.additionsList?.length != 0 && - )} + } ); }; diff --git a/www/js/diary/cards/UntrackedTimeCard.tsx b/www/js/diary/cards/UntrackedTimeCard.tsx index 07b5caf71..855c50ed4 100644 --- a/www/js/diary/cards/UntrackedTimeCard.tsx +++ b/www/js/diary/cards/UntrackedTimeCard.tsx @@ -7,57 +7,42 @@ UntrackedTimeCards use the reddish 'untracked' theme flavor. */ -import React from 'react'; +import React from "react"; import { View, StyleSheet } from 'react-native'; import { Text } from 'react-native-paper'; -import { getTheme } from '../../appTheme'; -import { useTranslation } from 'react-i18next'; -import { DiaryCard, cardStyles } from './DiaryCard'; -import { useAddressNames } from '../addressNamesHelper'; -import useDerivedProperties from '../useDerivedProperties'; -import StartEndLocations from '../components/StartEndLocations'; +import { getTheme } from "../../appTheme"; +import { useTranslation } from "react-i18next"; +import { DiaryCard, cardStyles } from "./DiaryCard"; +import { useAddressNames } from "../addressNamesHelper"; +import useDerivedProperties from "../useDerivedProperties"; +import StartEndLocations from "../components/StartEndLocations"; -type Props = { triplike: { [key: string]: any } }; +type Props = { triplike: {[key: string]: any}}; const UntrackedTimeCard = ({ triplike }: Props) => { const { t } = useTranslation(); const { displayStartTime, displayEndTime, displayDate } = useDerivedProperties(triplike); - const [triplikeStartDisplayName, triplikeEndDisplayName] = useAddressNames(triplike); + const [ triplikeStartDisplayName, triplikeEndDisplayName ] = useAddressNames(triplike); const flavoredTheme = getTheme('untracked'); return ( - - - {/* date and distance */} + + {/* date and distance */} - - {displayDate} - + {displayDate} - - + + {t('diary.untracked-time-range', { start: displayStartTime, end: displayEndTime })} - - {/* start and end locations */} - {/* start and end locations */} + + displayEndName={triplikeEndDisplayName} /> @@ -69,7 +54,7 @@ const s = StyleSheet.create({ borderRadius: 5, paddingVertical: 1, paddingHorizontal: 8, - fontSize: 13, + fontSize: 13 }, locationText: { fontSize: 12, diff --git a/www/js/diary/components/StartEndLocations.tsx b/www/js/diary/components/StartEndLocations.tsx index b25facc57..8d1096fab 100644 --- a/www/js/diary/components/StartEndLocations.tsx +++ b/www/js/diary/components/StartEndLocations.tsx @@ -4,70 +4,67 @@ import { Icon } from '../../components/Icon'; import { Text, Divider, useTheme } from 'react-native-paper'; type Props = { - displayStartTime?: string; - displayStartName: string; - displayEndTime?: string; - displayEndName?: string; - centered?: boolean; - fontSize?: number; + displayStartTime?: string, displayStartName: string, + displayEndTime?: string, displayEndName?: string, + centered?: boolean, + fontSize?: number, }; const StartEndLocations = (props: Props) => { + const { colors } = useTheme(); const fontSize = props.fontSize || 12; - return ( - <> + return (<> + + {props.displayStartTime && + + {props.displayStartTime} + + } + + + + + {props.displayStartName} + + + {(props.displayEndName != undefined) && <> + - {props.displayStartTime && ( - {props.displayStartTime} - )} - - + {props.displayEndTime && + + {props.displayEndTime} + + } + + - - {props.displayStartName} + + {props.displayEndName} - {props.displayEndName != undefined && ( - <> - - - {props.displayEndTime && ( - {props.displayEndTime} - )} - - - - - {props.displayEndName} - - - - )} - - ); -}; + } + ); +} const s = { - location: (centered) => - ({ - flexDirection: 'row', - alignItems: 'center', - justifyContent: centered ? 'center' : 'flex-start', - }) as ViewProps, - locationIcon: (colors, iconSize, filled?) => - ({ - border: `2px solid ${colors.primary}`, - borderRadius: 50, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: iconSize * 1.5, - height: iconSize * 1.5, - backgroundColor: filled ? colors.primary : colors.onPrimary, - marginRight: 6, - }) as ViewProps, -}; + location: (centered) => ({ + flexDirection: 'row', + alignItems: 'center', + justifyContent: centered ? 'center' : 'flex-start', + } as ViewProps), + locationIcon: (colors, iconSize, filled?) => ({ + border: `2px solid ${colors.primary}`, + borderRadius: 50, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: iconSize * 1.5, + height: iconSize * 1.5, + backgroundColor: filled ? colors.primary : colors.onPrimary, + marginRight: 6, + } as ViewProps) +} export default StartEndLocations; diff --git a/www/js/diary/details/LabelDetailsScreen.tsx b/www/js/diary/details/LabelDetailsScreen.tsx index ed48f89c9..ffed9a300 100644 --- a/www/js/diary/details/LabelDetailsScreen.tsx +++ b/www/js/diary/details/LabelDetailsScreen.tsx @@ -2,32 +2,25 @@ listed sections of the trip, and a graph of speed during the trip. Navigated to from the main LabelListScreen by clicking a trip card. */ -import React, { useContext, useState } from 'react'; -import { View, Modal, ScrollView, useWindowDimensions } from 'react-native'; -import { - PaperProvider, - Appbar, - SegmentedButtons, - Button, - Surface, - Text, - useTheme, -} from 'react-native-paper'; -import { LabelTabContext } from '../LabelTab'; -import LeafletView from '../../components/LeafletView'; -import { useTranslation } from 'react-i18next'; -import MultilabelButtonGroup from '../../survey/multilabel/MultiLabelButtonGroup'; -import UserInputButton from '../../survey/enketo/UserInputButton'; -import { useAddressNames } from '../addressNamesHelper'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import useDerivedProperties from '../useDerivedProperties'; -import StartEndLocations from '../components/StartEndLocations'; -import { useGeojsonForTrip } from '../timelineHelper'; -import TripSectionsDescriptives from './TripSectionsDescriptives'; -import OverallTripDescriptives from './OverallTripDescriptives'; -import ToggleSwitch from '../../components/ToggleSwitch'; +import React, { useContext, useState } from "react"; +import { View, Modal, ScrollView, useWindowDimensions } from "react-native"; +import { PaperProvider, Appbar, SegmentedButtons, Button, Surface, Text, useTheme } from "react-native-paper"; +import { LabelTabContext } from "../LabelTab"; +import LeafletView from "../../components/LeafletView"; +import { useTranslation } from "react-i18next"; +import MultilabelButtonGroup from "../../survey/multilabel/MultiLabelButtonGroup"; +import UserInputButton from "../../survey/enketo/UserInputButton"; +import { useAddressNames } from "../addressNamesHelper"; +import { SafeAreaView } from "react-native-safe-area-context"; +import useDerivedProperties from "../useDerivedProperties"; +import StartEndLocations from "../components/StartEndLocations"; +import { useGeojsonForTrip } from "../timelineHelper"; +import TripSectionsDescriptives from "./TripSectionsDescriptives"; +import OverallTripDescriptives from "./OverallTripDescriptives"; +import ToggleSwitch from "../../components/ToggleSwitch"; const LabelScreenDetails = ({ route, navigation }) => { + const { surveyOpt, timelineMap, labelOptions } = useContext(LabelTabContext); const { t } = useTranslation(); const { height: windowHeight } = useWindowDimensions(); @@ -35,91 +28,58 @@ const LabelScreenDetails = ({ route, navigation }) => { const trip = timelineMap.get(tripId); const { colors } = flavoredTheme || useTheme(); const { displayDate, displayStartTime, displayEndTime } = useDerivedProperties(trip); - const [tripStartDisplayName, tripEndDisplayName] = useAddressNames(trip); + const [ tripStartDisplayName, tripEndDisplayName ] = useAddressNames(trip); - const [modesShown, setModesShown] = useState<'labeled' | 'detected'>('labeled'); - const tripGeojson = useGeojsonForTrip( - trip, - labelOptions, - modesShown == 'labeled' && trip?.userInput?.MODE?.value, - ); - const mapOpts = { minZoom: 3, maxZoom: 17 }; + const [ modesShown, setModesShown ] = useState<'labeled'|'detected'>('labeled'); + const tripGeojson = useGeojsonForTrip(trip, labelOptions, modesShown=='labeled' && trip?.userInput?.MODE?.value); + const mapOpts = {minZoom: 3, maxZoom: 17}; const modal = ( - - - { - navigation.goBack(); - }} - /> - + + + { navigation.goBack() }} /> + - - + + - + {/* MultiLabel or UserInput button, inline on one row */} - {surveyOpt?.elementTag == 'multilabel' && ( - - )} - {surveyOpt?.elementTag == 'enketo-trip-button' && ( - - )} + {surveyOpt?.elementTag == 'multilabel' && + } + {surveyOpt?.elementTag == 'enketo-trip-button' + && } {/* Full-size Leaflet map, with zoom controls */} - + {/* If trip is labeled, show a toggle to switch between "Labeled Mode" and "Detected Modes" otherwise, just show "Detected" */} - {trip?.userInput?.MODE?.value ? ( - setModesShown(v)} - value={modesShown} - density="medium" - buttons={[ - { label: t('diary.labeled-mode'), value: 'labeled' }, - { label: t('diary.detected-modes'), value: 'detected' }, - ]} - /> - ) : ( - - )} + } {/* section-by-section breakdown of duration, distance, and mode */} - + {/* Overall trip duration, distance, and modes. Only show this when multiple sections are shown, and we are showing detected modes. If we just showed the labeled mode or a single section, this would be redundant. */} - {modesShown == 'detected' && trip?.sections?.length > 1 && ( + { modesShown == 'detected' && trip?.sections?.length > 1 && - )} + } {/* TODO: show speed graph here */} @@ -127,9 +87,13 @@ const LabelScreenDetails = ({ route, navigation }) => { ); if (route.params.flavoredTheme) { - return {modal}; + return ( + + {modal} + + ); } return modal; -}; +} export default LabelScreenDetails; diff --git a/www/js/diary/details/OverallTripDescriptives.tsx b/www/js/diary/details/OverallTripDescriptives.tsx index 8030842df..3902c8afe 100644 --- a/www/js/diary/details/OverallTripDescriptives.tsx +++ b/www/js/diary/details/OverallTripDescriptives.tsx @@ -1,45 +1,42 @@ import React from 'react'; import { View } from 'react-native'; -import { Text } from 'react-native-paper'; +import { Text } from 'react-native-paper' import useDerivedProperties from '../useDerivedProperties'; import { Icon } from '../../components/Icon'; import { useTranslation } from 'react-i18next'; const OverallTripDescriptives = ({ trip }) => { + const { t } = useTranslation(); - const { - displayStartTime, - displayEndTime, - displayTime, - formattedDistance, - distanceSuffix, - detectedModes, - } = useDerivedProperties(trip); + const { displayStartTime, displayEndTime, displayTime, + formattedDistance, distanceSuffix, detectedModes } = useDerivedProperties(trip); return ( - - Overall - + Overall - {displayTime} - {`${displayStartTime} - ${displayEndTime}`} + {displayTime} + {`${displayStartTime} - ${displayEndTime}`} - {`${formattedDistance} ${distanceSuffix}`} + + {`${formattedDistance} ${distanceSuffix}`} + {detectedModes?.map?.((pct, i) => ( - {pct.pct}% + + {pct.pct}% + ))} ); -}; +} export default OverallTripDescriptives; diff --git a/www/js/diary/details/TripSectionsDescriptives.tsx b/www/js/diary/details/TripSectionsDescriptives.tsx index 5bd30fdd5..6d172fed4 100644 --- a/www/js/diary/details/TripSectionsDescriptives.tsx +++ b/www/js/diary/details/TripSectionsDescriptives.tsx @@ -1,82 +1,65 @@ import React, { useContext } from 'react'; import { View } from 'react-native'; -import { Text, useTheme } from 'react-native-paper'; +import { Text, useTheme } from 'react-native-paper' import { Icon } from '../../components/Icon'; import useDerivedProperties from '../useDerivedProperties'; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from '../diaryHelper'; import { LabelTabContext } from '../LabelTab'; -const TripSectionsDescriptives = ({ trip, showLabeledMode = false }) => { +const TripSectionsDescriptives = ({ trip, showLabeledMode=false }) => { + const { labelOptions } = useContext(LabelTabContext); - const { - displayStartTime, - displayTime, - formattedDistance, - distanceSuffix, - formattedSectionProperties, - } = useDerivedProperties(trip); + const { displayStartTime, displayTime, formattedDistance, + distanceSuffix, formattedSectionProperties } = useDerivedProperties(trip); const { colors } = useTheme(); let sections = formattedSectionProperties; /* if we're only showing the labeled mode, or there are no sections (i.e. unprocessed trip), we treat this as unimodal and use trip-level attributes to construct a single section */ - if ((showLabeledMode && trip?.userInput?.MODE) || !trip.sections?.length) { + if (showLabeledMode && trip?.userInput?.MODE || !trip.sections?.length) { let baseMode; if (showLabeledMode && trip?.userInput?.MODE) { baseMode = getBaseModeOfLabeledTrip(trip, labelOptions); } else { baseMode = getBaseModeByKey('UNPROCESSED'); } - sections = [ - { - startTime: displayStartTime, - duration: displayTime, - distance: formattedDistance, - color: baseMode.color, - icon: baseMode.icon, - text: showLabeledMode && trip.userInput?.MODE?.text, // label text only shown for labeled trips - }, - ]; + sections = [{ + startTime: displayStartTime, + duration: displayTime, + distance: formattedDistance, + color: baseMode.color, + icon: baseMode.icon, + text: showLabeledMode && trip.userInput?.MODE?.text, // label text only shown for labeled trips + }]; } return ( {sections.map((section, i) => ( - + - {section.duration} - {section.startTime} + {section.duration} + {section.startTime} - {`${section.distance} ${distanceSuffix}`} + + {`${section.distance} ${distanceSuffix}`} + - - - {section.text && ( - + + + {section.text && + {section.text} - )} + } ))} ); -}; +} export default TripSectionsDescriptives; diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 48f40322d..0b834a485 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -1,67 +1,57 @@ // here we have some helper functions used throughout the label tab // these functions are being gradually migrated out of services.js -import moment from 'moment'; -import { DateTime } from 'luxon'; -import { LabelOptions, readableLabelToKey } from '../survey/multilabel/confirmHelper'; +import moment from "moment"; +import { DateTime } from "luxon"; +import { LabelOptions, readableLabelToKey } from "../survey/multilabel/confirmHelper"; export const modeColors = { - pink: '#c32e85', // oklch(56% 0.2 350) // e-car - red: '#c21725', // oklch(52% 0.2 25) // car - orange: '#bf5900', // oklch(58% 0.16 50) // air, hsr - green: '#008148', // oklch(53% 0.14 155) // bike, e-bike, moped - blue: '#0074b7', // oklch(54% 0.14 245) // walk - periwinkle: '#6356bf', // oklch(52% 0.16 285) // light rail, train, tram, subway - magenta: '#9240a4', // oklch(52% 0.17 320) // bus - grey: '#555555', // oklch(45% 0 0) // unprocessed / unknown - taupe: '#7d585a', // oklch(50% 0.05 15) // ferry, trolleybus, user-defined modes -}; + pink: '#c32e85', // oklch(56% 0.2 350) // e-car + red: '#c21725', // oklch(52% 0.2 25) // car + orange: '#bf5900', // oklch(58% 0.16 50) // air, hsr + green: '#008148', // oklch(53% 0.14 155) // bike, e-bike, moped + blue: '#0074b7', // oklch(54% 0.14 245) // walk + periwinkle: '#6356bf', // oklch(52% 0.16 285) // light rail, train, tram, subway + magenta: '#9240a4', // oklch(52% 0.17 320) // bus + grey: '#555555', // oklch(45% 0 0) // unprocessed / unknown + taupe: '#7d585a', // oklch(50% 0.05 15) // ferry, trolleybus, user-defined modes +} type BaseMode = { - name: string; - icon: string; - color: string; -}; + name: string, + icon: string, + color: string +} // parallels the server-side MotionTypes enum: https://github.com/e-mission/e-mission-server/blob/94e7478e627fa8c171323662f951c611c0993031/emission/core/wrapper/motionactivity.py#L12 -type MotionTypeKey = - | 'IN_VEHICLE' - | 'BICYCLING' - | 'ON_FOOT' - | 'STILL' - | 'UNKNOWN' - | 'TILTING' - | 'WALKING' - | 'RUNNING' - | 'NONE' - | 'STOPPED_WHILE_IN_VEHICLE' - | 'AIR_OR_HSR'; - -const BaseModes: { [k: string]: BaseMode } = { +type MotionTypeKey = 'IN_VEHICLE' | 'BICYCLING' | 'ON_FOOT' | 'STILL' | 'UNKNOWN' | 'TILTING' + | 'WALKING' | 'RUNNING' | 'NONE' | 'STOPPED_WHILE_IN_VEHICLE' | 'AIR_OR_HSR'; + +const BaseModes: {[k: string]: BaseMode} = { // BEGIN MotionTypes - IN_VEHICLE: { name: 'IN_VEHICLE', icon: 'speedometer', color: modeColors.red }, - BICYCLING: { name: 'BICYCLING', icon: 'bike', color: modeColors.green }, - ON_FOOT: { name: 'ON_FOOT', icon: 'walk', color: modeColors.blue }, - UNKNOWN: { name: 'UNKNOWN', icon: 'help', color: modeColors.grey }, - WALKING: { name: 'WALKING', icon: 'walk', color: modeColors.blue }, - AIR_OR_HSR: { name: 'AIR_OR_HSR', icon: 'airplane', color: modeColors.orange }, + IN_VEHICLE: { name: "IN_VEHICLE", icon: "speedometer", color: modeColors.red }, + BICYCLING: { name: "BICYCLING", icon: "bike", color: modeColors.green }, + ON_FOOT: { name: "ON_FOOT", icon: "walk", color: modeColors.blue }, + UNKNOWN: { name: "UNKNOWN", icon: "help", color: modeColors.grey }, + WALKING: { name: "WALKING", icon: "walk", color: modeColors.blue }, + AIR_OR_HSR: { name: "AIR_OR_HSR", icon: "airplane", color: modeColors.orange }, // END MotionTypes - CAR: { name: 'CAR', icon: 'car', color: modeColors.red }, - E_CAR: { name: 'E_CAR', icon: 'car-electric', color: modeColors.pink }, - E_BIKE: { name: 'E_BIKE', icon: 'bicycle-electric', color: modeColors.green }, - E_SCOOTER: { name: 'E_SCOOTER', icon: 'scooter-electric', color: modeColors.periwinkle }, - MOPED: { name: 'MOPED', icon: 'moped', color: modeColors.green }, - TAXI: { name: 'TAXI', icon: 'taxi', color: modeColors.red }, - BUS: { name: 'BUS', icon: 'bus-side', color: modeColors.magenta }, - AIR: { name: 'AIR', icon: 'airplane', color: modeColors.orange }, - LIGHT_RAIL: { name: 'LIGHT_RAIL', icon: 'train-car-passenger', color: modeColors.periwinkle }, - TRAIN: { name: 'TRAIN', icon: 'train-car-passenger', color: modeColors.periwinkle }, - TRAM: { name: 'TRAM', icon: 'fas fa-tram', color: modeColors.periwinkle }, - SUBWAY: { name: 'SUBWAY', icon: 'subway-variant', color: modeColors.periwinkle }, - FERRY: { name: 'FERRY', icon: 'ferry', color: modeColors.taupe }, - TROLLEYBUS: { name: 'TROLLEYBUS', icon: 'bus-side', color: modeColors.taupe }, - UNPROCESSED: { name: 'UNPROCESSED', icon: 'help', color: modeColors.grey }, - OTHER: { name: 'OTHER', icon: 'pencil-circle', color: modeColors.taupe }, + CAR: { name: "CAR", icon: "car", color: modeColors.red }, + E_CAR: { name: "E_CAR", icon: "car-electric", color: modeColors.pink }, + E_BIKE: { name: "E_BIKE", icon: "bicycle-electric", color: modeColors.green }, + E_SCOOTER: { name: "E_SCOOTER", icon: "scooter-electric", color: modeColors.periwinkle }, + MOPED: { name: "MOPED", icon: "moped", color: modeColors.green }, + TAXI: { name: "TAXI", icon: "taxi", color: modeColors.red }, + BUS: { name: "BUS", icon: "bus-side", color: modeColors.magenta }, + AIR: { name: "AIR", icon: "airplane", color: modeColors.orange }, + LIGHT_RAIL: { name: "LIGHT_RAIL", icon: "train-car-passenger", color: modeColors.periwinkle }, + TRAIN: { name: "TRAIN", icon: "train-car-passenger", color: modeColors.periwinkle }, + TRAM: { name: "TRAM", icon: "fas fa-tram", color: modeColors.periwinkle }, + SUBWAY: { name: "SUBWAY", icon: "subway-variant", color: modeColors.periwinkle }, + FERRY: { name: "FERRY", icon: "ferry", color: modeColors.taupe }, + TROLLEYBUS: { name: "TROLLEYBUS", icon: "bus-side", color: modeColors.taupe }, + UNPROCESSED: { name: "UNPROCESSED", icon: "help", color: modeColors.grey }, + OTHER: { name: "OTHER", icon: "pencil-circle", color: modeColors.taupe }, }; type BaseModeKey = keyof typeof BaseModes; @@ -69,29 +59,27 @@ type BaseModeKey = keyof typeof BaseModes; * @param motionName A string like "WALKING" or "MotionTypes.WALKING" * @returns A BaseMode object containing the name, icon, and color of the motion type */ -export function getBaseModeByKey( - motionName: BaseModeKey | MotionTypeKey | `MotionTypes.${MotionTypeKey}`, -) { +export function getBaseModeByKey(motionName: BaseModeKey | MotionTypeKey | `MotionTypes.${MotionTypeKey}`) { let key = ('' + motionName).toUpperCase(); - key = key.split('.').pop(); // if "MotionTypes.WALKING", then just take "WALKING" + key = key.split(".").pop(); // if "MotionTypes.WALKING", then just take "WALKING" return BaseModes[key] || BaseModes.UNKNOWN; } export function getBaseModeOfLabeledTrip(trip, labelOptions) { const modeKey = trip?.userInput?.MODE?.value; if (!modeKey) return null; // trip has no MODE label - const modeOption = labelOptions?.MODE?.find((opt) => opt.value == modeKey); - return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); + const modeOption = labelOptions?.MODE?.find(opt => opt.value == modeKey); + return getBaseModeByKey(modeOption?.baseMode || "OTHER"); } export function getBaseModeByValue(value, labelOptions: LabelOptions) { - const modeOption = labelOptions?.MODE?.find((opt) => opt.value == value); - return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); + const modeOption = labelOptions?.MODE?.find(opt => opt.value == value); + return getBaseModeByKey(modeOption?.baseMode || "OTHER"); } export function getBaseModeByText(text, labelOptions: LabelOptions) { - const modeOption = labelOptions?.MODE?.find((opt) => opt.text == text); - return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); + const modeOption = labelOptions?.MODE?.find(opt => opt.text == text); + return getBaseModeByKey(modeOption?.baseMode || "OTHER"); } /** @@ -102,10 +90,7 @@ export function getBaseModeByText(text, labelOptions: LabelOptions) { */ export function isMultiDay(beginFmtTime: string, endFmtTime: string) { if (!beginFmtTime || !endFmtTime) return false; - return ( - moment.parseZone(beginFmtTime).format('YYYYMMDD') != - moment.parseZone(endFmtTime).format('YYYYMMDD') - ); + return moment.parseZone(beginFmtTime).format('YYYYMMDD') != moment.parseZone(endFmtTime).format('YYYYMMDD'); } /** @@ -153,10 +138,11 @@ export function getFormattedTimeRange(beginFmtTime: string, endFmtTime: string) const beginMoment = moment.parseZone(beginFmtTime); const endMoment = moment.parseZone(endFmtTime); return endMoment.to(beginMoment, true); -} +}; // Temporary function to avoid repear in getDetectedModes ret val. -const filterRunning = (mode) => (mode == 'MotionTypes.RUNNING' ? 'MotionTypes.WALKING' : mode); +const filterRunning = (mode) => + (mode == 'MotionTypes.RUNNING') ? 'MotionTypes.WALKING' : mode; export function getDetectedModes(trip) { if (!trip.sections?.length) return []; @@ -171,16 +157,14 @@ export function getDetectedModes(trip) { }); // sort modes by the distance traveled (descending) - const sortedKeys = Object.entries(dists) - .sort((a, b) => b[1] - a[1]) - .map((e) => e[0]); + const sortedKeys = Object.entries(dists).sort((a, b) => b[1] - a[1]).map(e => e[0]); let sectionPcts = sortedKeys.map(function (mode) { const fract = dists[mode] / totalDist; return { mode: mode, icon: getBaseModeByKey(mode)?.icon, color: getBaseModeByKey(mode)?.color || 'black', - pct: Math.round(fract * 100) || '<1', // if rounds to 0%, show <1% + pct: Math.round(fract * 100) || '<1' // if rounds to 0%, show <1% }; }); @@ -194,7 +178,7 @@ export function getFormattedSectionProperties(trip, ImperialConfig) { distance: ImperialConfig.getFormattedDistance(s.distance), distanceSuffix: ImperialConfig.distanceSuffix, icon: getBaseModeByKey(s.sensed_mode_str)?.icon, - color: getBaseModeByKey(s.sensed_mode_str)?.color || '#333', + color: getBaseModeByKey(s.sensed_mode_str)?.color || "#333", })); } @@ -202,6 +186,6 @@ export function getLocalTimeString(dt) { if (!dt) return; /* correcting the date of the processed trips knowing that local_dt months are from 1 -> 12 and for the moment function they need to be between 0 -> 11 */ - const mdt = { ...dt, month: dt.month - 1 }; - return moment(mdt).format('LT'); + const mdt = { ...dt, month: dt.month-1 }; + return moment(mdt).format("LT"); } diff --git a/www/js/diary/diaryTypes.ts b/www/js/diary/diaryTypes.ts index 5755c91ab..bcaeb83ae 100644 --- a/www/js/diary/diaryTypes.ts +++ b/www/js/diary/diaryTypes.ts @@ -10,63 +10,63 @@ type ConfirmedPlace = any; // TODO /* These are the properties received from the server (basically matches Python code) This should match what Timeline.readAllCompositeTrips returns (an array of these objects) */ export type CompositeTrip = { - _id: { $oid: string }; - additions: any[]; // TODO - cleaned_section_summary: any; // TODO - cleaned_trip: { $oid: string }; - confidence_threshold: number; - confirmed_trip: { $oid: string }; - distance: number; - duration: number; - end_confirmed_place: ConfirmedPlace; - end_fmt_time: string; - end_loc: { type: string; coordinates: number[] }; - end_local_dt: any; // TODO - end_place: { $oid: string }; - end_ts: number; - expectation: any; // TODO "{to_label: boolean}" - expected_trip: { $oid: string }; - inferred_labels: any[]; // TODO - inferred_section_summary: any; // TODO - inferred_trip: { $oid: string }; - key: string; - locations: any[]; // TODO - origin_key: string; - raw_trip: { $oid: string }; - sections: any[]; // TODO - source: string; - start_confirmed_place: ConfirmedPlace; - start_fmt_time: string; - start_loc: { type: string; coordinates: number[] }; - start_local_dt: any; // TODO - start_place: { $oid: string }; - start_ts: number; - user_input: any; // TODO -}; + _id: {$oid: string}, + additions: any[], // TODO + cleaned_section_summary: any, // TODO + cleaned_trip: {$oid: string}, + confidence_threshold: number, + confirmed_trip: {$oid: string}, + distance: number, + duration: number, + end_confirmed_place: ConfirmedPlace, + end_fmt_time: string, + end_loc: {type: string, coordinates: number[]}, + end_local_dt: any, // TODO + end_place: {$oid: string}, + end_ts: number, + expectation: any, // TODO "{to_label: boolean}" + expected_trip: {$oid: string}, + inferred_labels: any[], // TODO + inferred_section_summary: any, // TODO + inferred_trip: {$oid: string}, + key: string, + locations: any[], // TODO + origin_key: string, + raw_trip: {$oid: string}, + sections: any[], // TODO + source: string, + start_confirmed_place: ConfirmedPlace, + start_fmt_time: string, + start_loc: {type: string, coordinates: number[]}, + start_local_dt: any, // TODO + start_place: {$oid: string}, + start_ts: number, + user_input: any, // TODO +} /* These properties aren't received from the server, but are derived from the above properties. They are used in the UI to display trip/place details and are computed by the useDerivedProperties hook. */ export type DerivedProperties = { - displayDate: string; - displayStartTime: string; - displayEndTime: string; - displayTime: string; - displayStartDateAbbr: string; - displayEndDateAbbr: string; - formattedDistance: string; - formattedSectionProperties: any[]; // TODO - distanceSuffix: string; - detectedModes: { mode: string; icon: string; color: string; pct: number | string }[]; -}; + displayDate: string, + displayStartTime: string, + displayEndTime: string, + displayTime: string, + displayStartDateAbbr: string, + displayEndDateAbbr: string, + formattedDistance: string, + formattedSectionProperties: any[], // TODO + distanceSuffix: string, + detectedModes: { mode: string, icon: string, color: string, pct: number|string }[], +} /* These are the properties that are still filled in by some kind of 'populate' mechanism. It would simplify the codebase to just compute them where they're needed (using memoization when apt so performance is not impacted). */ export type PopulatedTrip = CompositeTrip & { - additionsList?: any[]; // TODO - finalInference?: any; // TODO - geojson?: any; // TODO - getNextEntry?: () => PopulatedTrip | ConfirmedPlace; - userInput?: any; // TODO - verifiability?: string; -}; + additionsList?: any[], // TODO + finalInference?: any, // TODO + geojson?: any, // TODO + getNextEntry?: () => PopulatedTrip | ConfirmedPlace, + userInput?: any, // TODO + verifiability?: string, +} diff --git a/www/js/diary/list/DateSelect.tsx b/www/js/diary/list/DateSelect.tsx index 515553851..1c28cdc2c 100644 --- a/www/js/diary/list/DateSelect.tsx +++ b/www/js/diary/list/DateSelect.tsx @@ -6,17 +6,18 @@ and allows the user to select a date. */ -import React, { useEffect, useState, useMemo, useContext } from 'react'; -import { StyleSheet } from 'react-native'; -import moment from 'moment'; -import { LabelTabContext } from '../LabelTab'; -import { DatePickerModal } from 'react-native-paper-dates'; -import { Text, Divider, useTheme } from 'react-native-paper'; -import i18next from 'i18next'; -import { useTranslation } from 'react-i18next'; -import NavBarButton from '../../components/NavBarButton'; +import React, { useEffect, useState, useMemo, useContext } from "react"; +import { StyleSheet } from "react-native"; +import moment from "moment"; +import { LabelTabContext } from "../LabelTab"; +import { DatePickerModal } from "react-native-paper-dates"; +import { Text, Divider, useTheme } from "react-native-paper"; +import i18next from "i18next"; +import { useTranslation } from "react-i18next"; +import NavBarButton from "../../components/NavBarButton"; const DateSelect = ({ tsRange, loadSpecificWeekFn }) => { + const { pipelineRange } = useContext(LabelTabContext); const { t } = useTranslation(); const { colors } = useTheme(); @@ -56,48 +57,36 @@ const DateSelect = ({ tsRange, loadSpecificWeekFn }) => { loadSpecificWeekFn(params.date); setOpen(false); }, - [setOpen, loadSpecificWeekFn], + [setOpen, loadSpecificWeekFn] ); const dateRangeEnd = dateRange[1] || t('diary.today'); - return ( - <> - setOpen(true)}> - {dateRange[0] && ( - <> - {dateRange[0]} - - - )} - {dateRangeEnd} - - - - ); + return (<> + setOpen(true)}> + {dateRange[0] && (<> + {dateRange[0]} + + )} + {dateRangeEnd} + + + ); }; export const s = StyleSheet.create({ divider: { width: 25, marginHorizontal: 'auto', - }, + } }); export default DateSelect; diff --git a/www/js/diary/list/FilterSelect.tsx b/www/js/diary/list/FilterSelect.tsx index 0018c1bc5..d1906f462 100644 --- a/www/js/diary/list/FilterSelect.tsx +++ b/www/js/diary/list/FilterSelect.tsx @@ -7,36 +7,36 @@ shows the available filters and allows the user to select one. */ -import React, { useState, useMemo } from 'react'; -import { Modal } from 'react-native'; -import { useTranslation } from 'react-i18next'; -import NavBarButton from '../../components/NavBarButton'; -import { RadioButton, Text, Dialog } from 'react-native-paper'; +import React, { useState, useMemo } from "react"; +import { Modal } from "react-native"; +import { useTranslation } from "react-i18next"; +import NavBarButton from "../../components/NavBarButton"; +import { RadioButton, Text, Dialog } from "react-native-paper"; const FilterSelect = ({ filters, setFilters, numListDisplayed, numListTotal }) => { + const { t } = useTranslation(); const [modalVisible, setModalVisible] = useState(false); - const selectedFilter = useMemo(() => filters?.find((f) => f.state)?.key || 'show-all', [filters]); + const selectedFilter = useMemo(() => filters?.find(f => f.state)?.key || 'show-all', [filters]); const labelDisplayText = useMemo(() => { - if (!filters) return '...'; - const selectedFilterObj = filters?.find((f) => f.state); - if (!selectedFilterObj) return t('diary.show-all') + ` (${numListTotal || 0})`; - return selectedFilterObj.text + ` (${numListDisplayed || 0}/${numListTotal || 0})`; + if (!filters) + return '...'; + const selectedFilterObj = filters?.find(f => f.state); + if (!selectedFilterObj) return t('diary.show-all') + ` (${numListTotal||0})`; + return selectedFilterObj.text + ` (${numListDisplayed||0}/${numListTotal||0})`; }, [filters, numListDisplayed, numListTotal]); function chooseFilter(filterKey) { if (filterKey == 'show-all') { - setFilters(filters.map((f) => ({ ...f, state: false }))); + setFilters(filters.map(f => ({ ...f, state: false }))); } else { - setFilters( - filters.map((f) => { - if (f.key === filterKey) { - return { ...f, state: true }; - } else { - return { ...f, state: false }; - } - }), - ); + setFilters(filters.map(f => { + if (f.key === filterKey) { + return { ...f, state: true }; + } else { + return { ...f, state: false }; + } + })); } /* We must wait to close the modal until this function is done running, else the click event might leak to the content behind the modal */ @@ -44,32 +44,28 @@ const FilterSelect = ({ filters, setFilters, numListDisplayed, numListTotal }) = the next event loop cycle */ } - return ( - <> - setModalVisible(true)}> - {labelDisplayText} - - setModalVisible(false)}> - setModalVisible(false)}> - {/* TODO - add title */} - {/* {t('diary.filter-travel')} */} - - chooseFilter(k)} value={selectedFilter}> - {filters.map((f) => ( - - ))} - - - - - - - ); + return (<> + setModalVisible(true)}> + + {labelDisplayText} + + + setModalVisible(false)}> + setModalVisible(false)}> + {/* TODO - add title */} + {/* {t('diary.filter-travel')} */} + + chooseFilter(k)} value={selectedFilter}> + {filters.map(f => ( + + ))} + + + + + + ); }; export default FilterSelect; diff --git a/www/js/diary/list/LabelListScreen.tsx b/www/js/diary/list/LabelListScreen.tsx index fe18c1dbb..0ed5f702b 100644 --- a/www/js/diary/list/LabelListScreen.tsx +++ b/www/js/diary/list/LabelListScreen.tsx @@ -1,61 +1,38 @@ -import React, { useContext } from 'react'; -import { View } from 'react-native'; -import { Appbar, useTheme } from 'react-native-paper'; -import DateSelect from './DateSelect'; -import FilterSelect from './FilterSelect'; -import TimelineScrollList from './TimelineScrollList'; -import { LabelTabContext } from '../LabelTab'; +import React, { useContext } from "react"; +import { View } from "react-native"; +import { Appbar, useTheme } from "react-native-paper"; +import DateSelect from "./DateSelect"; +import FilterSelect from "./FilterSelect"; +import TimelineScrollList from "./TimelineScrollList"; +import { LabelTabContext } from "../LabelTab"; const LabelListScreen = () => { - const { - filterInputs, - setFilterInputs, - timelineMap, - displayedEntries, - queriedRange, - loadSpecificWeek, - refresh, - pipelineRange, - loadAnotherWeek, - isLoading, - } = useContext(LabelTabContext); + + const { filterInputs, setFilterInputs, timelineMap, displayedEntries, + queriedRange, loadSpecificWeek, refresh, pipelineRange, + loadAnotherWeek, isLoading } = useContext(LabelTabContext); const { colors } = useTheme(); - return ( - <> - - - - refresh()} - accessibilityLabel="Refresh" - style={{ marginLeft: 'auto' }} - /> - - - - - - ); -}; + return (<> + + + + refresh()} accessibilityLabel="Refresh" + style={{marginLeft: 'auto'}} /> + + + + + ) +} export default LabelListScreen; diff --git a/www/js/diary/list/LoadMoreButton.tsx b/www/js/diary/list/LoadMoreButton.tsx index bfc298c31..05fa2ecd1 100644 --- a/www/js/diary/list/LoadMoreButton.tsx +++ b/www/js/diary/list/LoadMoreButton.tsx @@ -1,23 +1,18 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { Button, useTheme } from 'react-native-paper'; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { Button, useTheme } from "react-native-paper"; const LoadMoreButton = ({ children, onPressFn, ...otherProps }) => { const { colors } = useTheme(); return ( - ); -}; +} const s = StyleSheet.create({ container: { @@ -26,8 +21,8 @@ const s = StyleSheet.create({ }, btn: { maxHeight: 30, - justifyContent: 'center', - }, + justifyContent: 'center' + } }); export default LoadMoreButton; diff --git a/www/js/diary/list/TimelineScrollList.tsx b/www/js/diary/list/TimelineScrollList.tsx index 954a90db9..6dfd1e736 100644 --- a/www/js/diary/list/TimelineScrollList.tsx +++ b/www/js/diary/list/TimelineScrollList.tsx @@ -11,58 +11,51 @@ import { Icon } from '../../components/Icon'; const renderCard = ({ item: listEntry }) => { if (listEntry.origin_key.includes('trip')) { - return ; + return } else if (listEntry.origin_key.includes('place')) { - return ; + return } else if (listEntry.origin_key.includes('untracked')) { - return ; + return } }; -const separator = () => ; -const bigSpinner = ; -const smallSpinner = ; +const separator = () => +const bigSpinner = +const smallSpinner = type Props = { - listEntries: any[]; - queriedRange: any; - pipelineRange: any; - loadMoreFn: (direction: string) => void; - isLoading: boolean | string; -}; -const TimelineScrollList = ({ - listEntries, - queriedRange, - pipelineRange, - loadMoreFn, - isLoading, -}: Props) => { + listEntries: any[], + queriedRange: any, + pipelineRange: any, + loadMoreFn: (direction: string) => void, + isLoading: boolean | string +} +const TimelineScrollList = ({ listEntries, queriedRange, pipelineRange, loadMoreFn, isLoading }: Props) => { + const { t } = useTranslation(); // The way that FlashList inverts the scroll view means we have to reverse the order of items too const reversedListEntries = listEntries ? [...listEntries].reverse() : []; - const reachedPipelineStart = queriedRange?.start_ts <= pipelineRange?.start_ts; - const footer = ( - loadMoreFn('past')} disabled={reachedPipelineStart}> - {reachedPipelineStart ? t('diary.no-more-travel') : t('diary.show-older-travel')} - - ); - - const reachedPipelineEnd = queriedRange?.end_ts >= pipelineRange?.end_ts; - const header = ( - loadMoreFn('future')} disabled={reachedPipelineEnd}> - {reachedPipelineEnd ? t('diary.no-more-travel') : t('diary.show-more-travel')} - - ); + const reachedPipelineStart = (queriedRange?.start_ts <= pipelineRange?.start_ts); + const footer = loadMoreFn('past')} + disabled={reachedPipelineStart}> + { reachedPipelineStart ? t('diary.no-more-travel') : t('diary.show-older-travel')} + ; + + const reachedPipelineEnd = (queriedRange?.end_ts >= pipelineRange?.end_ts); + const header = loadMoreFn('future')} + disabled={reachedPipelineEnd}> + { reachedPipelineEnd ? t('diary.no-more-travel') : t('diary.show-more-travel')} + ; const noTravelBanner = ( - }> + + }> - {t('diary.no-travel')} - {t('diary.no-travel-hint')} + {t('diary.no-travel')} + {t('diary.no-travel-hint')} ); @@ -71,7 +64,7 @@ const TimelineScrollList = ({ /* Condition: pipelineRange has been fetched but has no defined end, meaning nothing has been processed for this OPCode yet, and there are no unprocessed trips either. Show 'no travel'. */ return noTravelBanner; - } else if (isLoading == 'replace') { + } else if (isLoading=='replace') { /* Condition: we're loading an entirely new batch of trips, so show a big spinner */ return bigSpinner; } else if (listEntries && listEntries.length == 0) { @@ -80,8 +73,7 @@ const TimelineScrollList = ({ } else if (listEntries) { /* Condition: we've successfully loaded and set `listEntries`, so show the list */ return ( - console.debug(e.nativeEvent.contentOffset.y)} - ListHeaderComponent={isLoading == 'append' ? smallSpinner : !reachedPipelineEnd && header} + ListHeaderComponent={isLoading == 'append' ? smallSpinner : (!reachedPipelineEnd && header)} ListFooterComponent={isLoading == 'prepend' ? smallSpinner : footer} - ItemSeparatorComponent={separator} - /> + ItemSeparatorComponent={separator} /> ); } -}; +} export default TimelineScrollList; diff --git a/www/js/diary/services.js b/www/js/diary/services.js index 5dcc0549f..0891b9103 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -4,58 +4,47 @@ import angular from 'angular'; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from './diaryHelper'; import { SurveyOptions } from '../survey/survey'; -angular - .module('emission.main.diary.services', ['emission.plugin.logger', 'emission.services']) - .factory( - 'Timeline', - function ( - CommHelper, - DynamicConfig, - $http, - $ionicLoading, - $ionicPlatform, - $window, - $rootScope, - UnifiedDataLoader, - Logger, - $injector, - ) { - var timeline = {}; - // corresponds to the old $scope.data. Contains all state for the current - // day, including the indication of the current day - timeline.data = {}; - timeline.data.unifiedConfirmsResults = null; - timeline.UPDATE_DONE = 'TIMELINE_UPDATE_DONE'; - - let manualInputFactory; - $ionicPlatform.ready(function () { - DynamicConfig.configReady().then((configObj) => { - const surveyOptKey = configObj.survey_info['trip-labels']; - const surveyOpt = SurveyOptions[surveyOptKey]; - console.log('surveyOpt in services.js is', surveyOpt); - manualInputFactory = $injector.get(surveyOpt.service); - }); +angular.module('emission.main.diary.services', ['emission.plugin.logger', + 'emission.services']) +.factory('Timeline', function(CommHelper, DynamicConfig, $http, $ionicLoading, $ionicPlatform, $window, + $rootScope, UnifiedDataLoader, Logger, $injector) { + var timeline = {}; + // corresponds to the old $scope.data. Contains all state for the current + // day, including the indication of the current day + timeline.data = {}; + timeline.data.unifiedConfirmsResults = null; + timeline.UPDATE_DONE = "TIMELINE_UPDATE_DONE"; + + let manualInputFactory; + $ionicPlatform.ready(function () { + DynamicConfig.configReady().then((configObj) => { + const surveyOptKey = configObj.survey_info['trip-labels']; + const surveyOpt = SurveyOptions[surveyOptKey]; + console.log('surveyOpt in services.js is', surveyOpt); + manualInputFactory = $injector.get(surveyOpt.service); }); - - // DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. - // This function returns a shallow copy of the obj, which flattens the - // 'data' field into the top level, while also including '_id' and 'metadata.key' - const unpack = (obj) => ({ - ...obj.data, - _id: obj._id, - key: obj.metadata.key, - origin_key: obj.metadata.origin_key || obj.metadata.key, + }); + + // DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. + // This function returns a shallow copy of the obj, which flattens the + // 'data' field into the top level, while also including '_id' and 'metadata.key' + const unpack = (obj) => ({ + ...obj.data, + _id: obj._id, + key: obj.metadata.key, + origin_key: obj.metadata.origin_key || obj.metadata.key, + }); + + timeline.readAllCompositeTrips = function(startTs, endTs) { + $ionicLoading.show({ + template: i18next.t('service.reading-server') }); - - timeline.readAllCompositeTrips = function (startTs, endTs) { - $ionicLoading.show({ - template: i18next.t('service.reading-server'), - }); - const readPromises = [ - CommHelper.getRawEntries(['analysis/composite_trip'], startTs, endTs, 'data.end_ts'), - ]; - return Promise.all(readPromises) - .then(([ctList]) => { + const readPromises = [ + CommHelper.getRawEntries(["analysis/composite_trip"], + startTs, endTs, "data.end_ts"), + ]; + return Promise.all(readPromises) + .then(([ctList]) => { $ionicLoading.hide(); return ctList.phone_data.map((ct) => { const unpackedCt = unpack(ct); @@ -65,222 +54,191 @@ angular end_confirmed_place: unpack(unpackedCt.end_confirmed_place), locations: unpackedCt.locations?.map(unpack), sections: unpackedCt.sections?.map(unpack), - }; + } }); - }) - .catch((err) => { - Logger.displayError('while reading confirmed trips', err); + }) + .catch((err) => { + Logger.displayError("while reading confirmed trips", err); $ionicLoading.hide(); return []; - }); - }; - - /* - * This is going to be a bit tricky. As we can see from - * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, - * when we read local transitions, they have a string for the transition - * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer - * (e.g. `2`). - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 - * - * Also, at least on iOS, it is possible for trip end to be detected way - * after the end of the trip, so the trip end transition of a processed - * trip may actually show up as an unprocessed transition. - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 - * - * Let's abstract this out into our own minor state machine. - */ - var transitions2Trips = function (transitionList) { + }); + }; + + /* + * This is going to be a bit tricky. As we can see from + * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, + * when we read local transitions, they have a string for the transition + * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer + * (e.g. `2`). + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 + * + * Also, at least on iOS, it is possible for trip end to be detected way + * after the end of the trip, so the trip end transition of a processed + * trip may actually show up as an unprocessed transition. + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 + * + * Let's abstract this out into our own minor state machine. + */ + var transitions2Trips = function(transitionList) { var inTrip = false; - var tripList = []; + var tripList = [] var currStartTransitionIndex = -1; var currEndTransitionIndex = -1; var processedUntil = 0; - - while (processedUntil < transitionList.length) { + + while(processedUntil < transitionList.length) { // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); - if (inTrip == false) { - var foundStartTransitionIndex = transitionList - .slice(processedUntil) - .findIndex(isStartingTransition); - if (foundStartTransitionIndex == -1) { - Logger.log('No further unprocessed trips started, exiting loop'); - processedUntil = transitionList.length; - } else { - currStartTransitionIndex = processedUntil + foundStartTransitionIndex; - processedUntil = currStartTransitionIndex; - Logger.log( - 'Unprocessed trip started at ' + - JSON.stringify(transitionList[currStartTransitionIndex]), - ); - inTrip = true; - } + if(inTrip == false) { + var foundStartTransitionIndex = transitionList.slice(processedUntil).findIndex(isStartingTransition); + if (foundStartTransitionIndex == -1) { + Logger.log("No further unprocessed trips started, exiting loop"); + processedUntil = transitionList.length; + } else { + currStartTransitionIndex = processedUntil + foundStartTransitionIndex; + processedUntil = currStartTransitionIndex; + Logger.log("Unprocessed trip started at "+JSON.stringify(transitionList[currStartTransitionIndex])); + inTrip = true; + } } else { - // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); - var foundEndTransitionIndex = transitionList - .slice(processedUntil) - .findIndex(isEndingTransition); - if (foundEndTransitionIndex == -1) { - Logger.log( - "Can't find end for trip starting at " + - JSON.stringify(transitionList[currStartTransitionIndex]) + - ' dropping it', - ); - processedUntil = transitionList.length; - } else { - currEndTransitionIndex = processedUntil + foundEndTransitionIndex; - processedUntil = currEndTransitionIndex; - Logger.log('currEndTransitionIndex = ' + currEndTransitionIndex); - Logger.log( - 'Unprocessed trip starting at ' + - JSON.stringify(transitionList[currStartTransitionIndex]) + - ' ends at ' + - JSON.stringify(transitionList[currEndTransitionIndex]), - ); - tripList.push([ - transitionList[currStartTransitionIndex], - transitionList[currEndTransitionIndex], - ]); - inTrip = false; - } + // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); + var foundEndTransitionIndex = transitionList.slice(processedUntil).findIndex(isEndingTransition); + if (foundEndTransitionIndex == -1) { + Logger.log("Can't find end for trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" dropping it"); + processedUntil = transitionList.length; + } else { + currEndTransitionIndex = processedUntil + foundEndTransitionIndex; + processedUntil = currEndTransitionIndex; + Logger.log("currEndTransitionIndex = "+currEndTransitionIndex); + Logger.log("Unprocessed trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" ends at "+JSON.stringify(transitionList[currEndTransitionIndex])); + tripList.push([transitionList[currStartTransitionIndex], + transitionList[currEndTransitionIndex]]) + inTrip = false; + } } } return tripList; - }; + } - var isStartingTransition = function (transWrapper) { + var isStartingTransition = function(transWrapper) { // Logger.log("isStartingTransition: transWrapper.data.transition = "+transWrapper.data.transition); - if ( - transWrapper.data.transition == 'local.transition.exited_geofence' || - transWrapper.data.transition == 'T_EXITED_GEOFENCE' || - transWrapper.data.transition == 1 - ) { - // Logger.log("Returning true"); - return true; + if(transWrapper.data.transition == 'local.transition.exited_geofence' || + transWrapper.data.transition == 'T_EXITED_GEOFENCE' || + transWrapper.data.transition == 1) { + // Logger.log("Returning true"); + return true; } // Logger.log("Returning false"); return false; - }; + } - var isEndingTransition = function (transWrapper) { + var isEndingTransition = function(transWrapper) { // Logger.log("isEndingTransition: transWrapper.data.transition = "+transWrapper.data.transition); - if ( - transWrapper.data.transition == 'T_TRIP_ENDED' || - transWrapper.data.transition == 'local.transition.stopped_moving' || - transWrapper.data.transition == 2 - ) { - // Logger.log("Returning true"); - return true; + if(transWrapper.data.transition == 'T_TRIP_ENDED' || + transWrapper.data.transition == 'local.transition.stopped_moving' || + transWrapper.data.transition == 2) { + // Logger.log("Returning true"); + return true; } // Logger.log("Returning false"); return false; - }; + } - /* - * Fill out place geojson after pulling trip location points. - * Place is only partially filled out because we haven't linked the timeline yet - */ + /* + * Fill out place geojson after pulling trip location points. + * Place is only partially filled out because we haven't linked the timeline yet + */ - var moment2localdate = function (currMoment, tz) { + var moment2localdate = function(currMoment, tz) { return { - timezone: tz, - year: currMoment.year(), - //the months of the draft trips match the one format needed for - //moment function however now that is modified we need to also - //modify the months value here - month: currMoment.month() + 1, - day: currMoment.date(), - weekday: currMoment.weekday(), - hour: currMoment.hour(), - minute: currMoment.minute(), - second: currMoment.second(), + timezone: tz, + year: currMoment.year(), + //the months of the draft trips match the one format needed for + //moment function however now that is modified we need to also + //modify the months value here + month: currMoment.month() + 1, + day: currMoment.date(), + weekday: currMoment.weekday(), + hour: currMoment.hour(), + minute: currMoment.minute(), + second: currMoment.second() }; - }; - - var points2TripProps = function (locationPoints) { - var startPoint = locationPoints[0]; - var endPoint = locationPoints[locationPoints.length - 1]; - var tripAndSectionId = 'unprocessed_' + startPoint.data.ts + '_' + endPoint.data.ts; - var startMoment = moment.unix(startPoint.data.ts).tz(startPoint.metadata.time_zone); - var endMoment = moment.unix(endPoint.data.ts).tz(endPoint.metadata.time_zone); - - const speeds = [], - dists = []; - let loc, locLatLng; - locationPoints.forEach((pt) => { - const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); - if (loc) { - const dist = locLatLng.distanceTo(ptLatLng); - const timeDelta = pt.data.ts - loc.data.ts; - dists.push(dist); - speeds.push(dist / timeDelta); - } - loc = pt; - locLatLng = ptLatLng; - }); - - const locations = locationPoints.map((point, i) => ({ + } + + var points2TripProps = function(locationPoints) { + var startPoint = locationPoints[0]; + var endPoint = locationPoints[locationPoints.length - 1]; + var tripAndSectionId = "unprocessed_"+startPoint.data.ts+"_"+endPoint.data.ts; + var startMoment = moment.unix(startPoint.data.ts).tz(startPoint.metadata.time_zone); + var endMoment = moment.unix(endPoint.data.ts).tz(endPoint.metadata.time_zone); + + const speeds = [], dists = []; + let loc, locLatLng; + locationPoints.forEach((pt) => { + const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); + if (loc) { + const dist = locLatLng.distanceTo(ptLatLng); + const timeDelta = pt.data.ts - loc.data.ts; + dists.push(dist); + speeds.push(dist / timeDelta); + } + loc = pt; + locLatLng = ptLatLng; + }); + + const locations = locationPoints.map((point, i) => ({ loc: { - coordinates: [point.data.longitude, point.data.latitude], + coordinates: [point.data.longitude, point.data.latitude] }, ts: point.data.ts, speed: speeds[i], - })); - - return { - _id: { $oid: tripAndSectionId }, - key: 'UNPROCESSED_trip', - origin_key: 'UNPROCESSED_trip', - additions: [], - confidence_threshold: 0, - distance: dists.reduce((a, b) => a + b, 0), - duration: endPoint.data.ts - startPoint.data.ts, - end_fmt_time: endMoment.format(), - end_local_dt: moment2localdate(endMoment, endPoint.metadata.time_zone), - end_ts: endPoint.data.ts, - expectation: { to_label: true }, - inferred_labels: [], - locations: locations, - source: 'unprocessed', - start_fmt_time: startMoment.format(), - start_local_dt: moment2localdate(startMoment, startPoint.metadata.time_zone), - start_ts: startPoint.data.ts, - user_input: {}, - }; - }; - - var tsEntrySort = function (e1, e2) { - // compare timestamps - return e1.data.ts - e2.data.ts; - }; - - var transitionTrip2TripObj = function (trip) { - var tripStartTransition = trip[0]; - var tripEndTransition = trip[1]; - var tq = { - key: 'write_ts', - startTs: tripStartTransition.data.ts, - endTs: tripEndTransition.data.ts, - }; - Logger.log( - 'About to pull location data for range ' + - moment.unix(tripStartTransition.data.ts).toString() + - ' -> ' + - moment.unix(tripEndTransition.data.ts).toString(), - ); - return UnifiedDataLoader.getUnifiedSensorDataForInterval( - 'background/filtered_location', - tq, - ).then(function (locationList) { + })); + + return { + _id: {$oid: tripAndSectionId}, + key: "UNPROCESSED_trip", + origin_key: "UNPROCESSED_trip", + additions: [], + confidence_threshold: 0, + distance: dists.reduce((a, b) => a + b, 0), + duration: endPoint.data.ts - startPoint.data.ts, + end_fmt_time: endMoment.format(), + end_local_dt: moment2localdate(endMoment, endPoint.metadata.time_zone), + end_ts: endPoint.data.ts, + expectation: {to_label: true}, + inferred_labels: [], + locations: locations, + source: "unprocessed", + start_fmt_time: startMoment.format(), + start_local_dt: moment2localdate(startMoment, startPoint.metadata.time_zone), + start_ts: startPoint.data.ts, + user_input: {}, + } + } + + var tsEntrySort = function(e1, e2) { + // compare timestamps + return e1.data.ts - e2.data.ts; + } + + var transitionTrip2TripObj = function(trip) { + var tripStartTransition = trip[0]; + var tripEndTransition = trip[1]; + var tq = {key: "write_ts", + startTs: tripStartTransition.data.ts, + endTs: tripEndTransition.data.ts + } + Logger.log("About to pull location data for range " + + moment.unix(tripStartTransition.data.ts).toString() + " -> " + + 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 - ); - }; + var retainInRange = function(loc) { + return (tripStartTransition.data.ts <= loc.data.ts) && + (loc.data.ts <= tripEndTransition.data.ts) + } var filteredLocationList = sortedLocationList.filter(retainInRange); @@ -290,26 +248,17 @@ angular } var tripStartPoint = filteredLocationList[0]; - var tripEndPoint = filteredLocationList[filteredLocationList.length - 1]; - Logger.log( - 'tripStartPoint = ' + - JSON.stringify(tripStartPoint) + - 'tripEndPoint = ' + - JSON.stringify(tripEndPoint), - ); + 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 + // 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), - ); + 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); @@ -317,130 +266,121 @@ angular return { ...tripProps, start_loc: { - type: 'Point', - coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude], + type: "Point", + coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude] }, end_loc: { - type: 'Point', + type: "Point", coordinates: [tripEndPoint.data.longitude, tripEndPoint.data.latitude], }, - }; + } }); - }; + } - var linkTrips = function (trip1, trip2) { + var linkTrips = function(trip1, trip2) { // complete trip1 - trip1.starting_trip = { $oid: trip2.id }; + trip1.starting_trip = {$oid: trip2.id}; trip1.exit_fmt_time = trip2.enter_fmt_time; trip1.exit_local_dt = trip2.enter_local_dt; trip1.exit_ts = trip2.enter_ts; // start trip2 - trip2.ending_trip = { $oid: trip1.id }; + trip2.ending_trip = {$oid: trip1.id}; trip2.enter_fmt_time = trip1.exit_fmt_time; trip2.enter_local_dt = trip1.exit_local_dt; trip2.enter_ts = trip1.exit_ts; - }; + } - timeline.readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) { + timeline.readUnprocessedTrips = function(startTs, endTs, lastProcessedTrip) { $ionicLoading.show({ - template: i18next.t('service.reading-unprocessed-data'), + template: i18next.t('service.reading-unprocessed-data') }); - var tq = { key: 'write_ts', startTs, endTs }; - Logger.log( - 'about to query for unprocessed trips from ' + - moment.unix(tq.startTs).toString() + - ' -> ' + - moment.unix(tq.endTs).toString(), - ); - return UnifiedDataLoader.getUnifiedMessagesForInterval('statemachine/transition', tq).then( - function (transitionList) { - if (transitionList.length == 0) { - Logger.log('No unprocessed trips. yay!'); - $ionicLoading.hide(); - return []; - } else { - Logger.log('Found ' + transitionList.length + ' transitions. yay!'); - var sortedTransitionList = transitionList.sort(tsEntrySort); - /* + var tq = {key: "write_ts", + startTs, + endTs + } + Logger.log("about to query for unprocessed trips from " + +moment.unix(tq.startTs).toString()+" -> "+moment.unix(tq.endTs).toString()); + return UnifiedDataLoader.getUnifiedMessagesForInterval("statemachine/transition", tq) + .then(function(transitionList) { + if (transitionList.length == 0) { + Logger.log("No unprocessed trips. yay!"); + $ionicLoading.hide(); + return []; + } else { + Logger.log("Found "+transitionList.length+" transitions. yay!"); + var sortedTransitionList = transitionList.sort(tsEntrySort); + /* sortedTransitionList.forEach(function(transition) { console.log(moment(transition.data.ts * 1000).format()+":" + JSON.stringify(transition.data)); }); */ - var tripsList = transitions2Trips(transitionList); - Logger.log('Mapped into' + tripsList.length + ' trips. yay!'); - tripsList.forEach(function (trip) { + var tripsList = transitions2Trips(transitionList); + Logger.log("Mapped into"+tripsList.length+" trips. yay!"); + tripsList.forEach(function(trip) { console.log(JSON.stringify(trip)); - }); - var tripFillPromises = tripsList.map(transitionTrip2TripObj); - return Promise.all(tripFillPromises).then(function (raw_trip_gj_list) { + }); + var tripFillPromises = tripsList.map(transitionTrip2TripObj); + return Promise.all(tripFillPromises).then(function(raw_trip_gj_list) { // Now we need to link up the trips. linking unprocessed trips // to one another is fairly simple, but we need to link the // first unprocessed trip to the last processed trip. // This might be challenging if we don't have any processed - // trips for the day. I don't want to go back forever until + // trips for the day. I don't want to go back forever until // I find a trip. So if this is the first trip, we will start a // new chain for now, since this is with unprocessed data // anyway. - Logger.log('mapped trips to trip_gj_list of size ' + raw_trip_gj_list.length); + Logger.log("mapped trips to trip_gj_list of size "+raw_trip_gj_list.length); /* Filtering: we will keep trips that are 1) defined and 2) have a distance >= 100m or duration >= 5 minutes https://github.com/e-mission/e-mission-docs/issues/966#issuecomment-1709112578 */ - const trip_gj_list = raw_trip_gj_list.filter( - (trip) => trip && (trip.distance >= 100 || trip.duration >= 300), - ); - Logger.log( - 'after filtering undefined and distance < 100, trip_gj_list size = ' + - raw_trip_gj_list.length, + const trip_gj_list = raw_trip_gj_list.filter((trip) => + trip && (trip.distance >= 100 || trip.duration >= 300) ); + Logger.log("after filtering undefined and distance < 100, trip_gj_list size = "+raw_trip_gj_list.length); // Link 0th trip to first, first to second, ... - for (var i = 0; i < trip_gj_list.length - 1; i++) { - linkTrips(trip_gj_list[i], trip_gj_list[i + 1]); + for (var i = 0; i < trip_gj_list.length-1; i++) { + linkTrips(trip_gj_list[i], trip_gj_list[i+1]); } - Logger.log('finished linking trips for list of size ' + trip_gj_list.length); + Logger.log("finished linking trips for list of size "+trip_gj_list.length); if (lastProcessedTrip && trip_gj_list.length != 0) { - // Need to link the entire chain above to the processed data - Logger.log('linking unprocessed and processed trip chains'); - linkTrips(lastProcessedTrip, trip_gj_list[0]); + // Need to link the entire chain above to the processed data + Logger.log("linking unprocessed and processed trip chains"); + linkTrips(lastProcessedTrip, trip_gj_list[0]); } $ionicLoading.hide(); - Logger.log('Returning final list of size ' + trip_gj_list.length); + Logger.log("Returning final list of size "+trip_gj_list.length); return trip_gj_list; - }); - } - }, - ); - }; + }); + } + }); + } - var localCacheReadFn = timeline.updateFromDatabase; + var localCacheReadFn = timeline.updateFromDatabase; - timeline.getTrip = function (tripId) { - return angular.isDefined(timeline.data.tripMap) ? timeline.data.tripMap[tripId] : undefined; + timeline.getTrip = function(tripId) { + return angular.isDefined(timeline.data.tripMap)? timeline.data.tripMap[tripId] : undefined; }; - timeline.getTripWrapper = function (tripId) { - return angular.isDefined(timeline.data.tripWrapperMap) - ? timeline.data.tripWrapperMap[tripId] - : undefined; + timeline.getTripWrapper = function(tripId) { + return angular.isDefined(timeline.data.tripWrapperMap)? timeline.data.tripWrapperMap[tripId] : undefined; }; - timeline.getCompositeTrip = function (tripId) { - return angular.isDefined(timeline.data.infScrollCompositeTripMap) - ? timeline.data.infScrollCompositeTripMap[tripId] - : undefined; + timeline.getCompositeTrip = function(tripId) { + return angular.isDefined(timeline.data.infScrollCompositeTripMap)? timeline.data.infScrollCompositeTripMap[tripId] : undefined; }; - timeline.setInfScrollCompositeTripList = function (compositeTripList) { + timeline.setInfScrollCompositeTripList = function(compositeTripList) { timeline.data.infScrollCompositeTripList = compositeTripList; timeline.data.infScrollCompositeTripMap = {}; - timeline.data.infScrollCompositeTripList.forEach(function (trip, index, array) { + timeline.data.infScrollCompositeTripList.forEach(function(trip, index, array) { timeline.data.infScrollCompositeTripMap[trip._id.$oid] = trip; }); - }; + } + + return timeline; + }) - return timeline; - }, - ); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index be6ee1bb3..579e3ac4f 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,8 +1,8 @@ -import moment from 'moment'; -import { getAngularService } from '../angular-react-helper'; -import { displayError, logDebug } from '../plugin/logger'; -import { getBaseModeByKey, getBaseModeOfLabeledTrip } from './diaryHelper'; -import i18next from 'i18next'; +import moment from "moment"; +import { getAngularService } from "../angular-react-helper"; +import { displayError, logDebug } from "../plugin/logger"; +import { getBaseModeByKey, getBaseModeOfLabeledTrip } from "./diaryHelper"; +import i18next from "i18next"; const cachedGeojsons = new Map(); /** @@ -15,29 +15,29 @@ export function useGeojsonForTrip(trip, labelOptions, labeledMode?) { return cachedGeojsons.get(gjKey); } - let trajectoryColor: string | null; + let trajectoryColor: string|null; if (labeledMode) { trajectoryColor = getBaseModeOfLabeledTrip(trip, labelOptions)?.color; } - logDebug("Reading trip's " + trip.locations.length + ' location points at ' + new Date()); + logDebug("Reading trip's " + trip.locations.length + " location points at " + (new Date())); var features = [ - location2GeojsonPoint(trip.start_loc, 'start_place'), - location2GeojsonPoint(trip.end_loc, 'end_place'), - ...locations2GeojsonTrajectory(trip, trip.locations, trajectoryColor), + location2GeojsonPoint(trip.start_loc, "start_place"), + location2GeojsonPoint(trip.end_loc, "end_place"), + ...locations2GeojsonTrajectory(trip, trip.locations, trajectoryColor) ]; const gj = { data: { id: gjKey, - type: 'FeatureCollection', + type: "FeatureCollection", features: features, properties: { start_ts: trip.start_ts, - end_ts: trip.end_ts, - }, - }, - }; + end_ts: trip.end_ts + } + } + } cachedGeojsons.set(gjKey, gj); return gj; } @@ -70,14 +70,7 @@ export function compositeTrips2TimelineMap(ctList: any[], unpackPlaces?: boolean return timelineEntriesMap; } -export function populateCompositeTrips( - ctList, - showPlaces, - labelsFactory, - labelsResultMap, - notesFactory, - notesResultMap, -) { +export function populateCompositeTrips(ctList, showPlaces, labelsFactory, labelsResultMap, notesFactory, notesResultMap) { try { ctList.forEach((ct, i) => { if (showPlaces && ct.start_confirmed_place) { @@ -104,9 +97,9 @@ export function populateCompositeTrips( } const getUnprocessedInputQuery = (pipelineRange) => ({ - key: 'write_ts', + key: "write_ts", startTs: pipelineRange.end_ts - 10, - endTs: moment().unix() + 10, + endTs: moment().unix() + 10 }); function getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises) { @@ -135,10 +128,10 @@ export function getLocalUnprocessedInputs(pipelineRange, labelsFactory, notesFac const BEMUserCache = window['cordova'].plugins.BEMUserCache; const tq = getUnprocessedInputQuery(pipelineRange); const labelsPromises = labelsFactory.MANUAL_KEYS.map((key) => - BEMUserCache.getMessagesForInterval(key, tq, true).then(labelsFactory.extractResult), + BEMUserCache.getMessagesForInterval(key, tq, true).then(labelsFactory.extractResult) ); const notesPromises = notesFactory.MANUAL_KEYS.map((key) => - BEMUserCache.getMessagesForInterval(key, tq, true).then(notesFactory.extractResult), + BEMUserCache.getMessagesForInterval(key, tq, true).then(notesFactory.extractResult) ); return getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises); } @@ -157,12 +150,10 @@ export function getAllUnprocessedInputs(pipelineRange, labelsFactory, notesFacto const UnifiedDataLoader = getAngularService('UnifiedDataLoader'); const tq = getUnprocessedInputQuery(pipelineRange); const labelsPromises = labelsFactory.MANUAL_KEYS.map((key) => - UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then( - labelsFactory.extractResult, - ), + UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then(labelsFactory.extractResult) ); const notesPromises = notesFactory.MANUAL_KEYS.map((key) => - UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then(notesFactory.extractResult), + UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then(notesFactory.extractResult) ); return getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises); } @@ -173,14 +164,14 @@ export function getAllUnprocessedInputs(pipelineRange, labelsFactory, notesFacto * @returns a GeoJSON feature with type "Point", the given location's coordinates and the given feature type */ const location2GeojsonPoint = (locationPoint: any, featureType: string) => ({ - type: 'Feature', + type: "Feature", geometry: { - type: 'Point', + type: "Point", coordinates: locationPoint.coordinates, }, properties: { feature_type: featureType, - }, + } }); /** @@ -197,23 +188,25 @@ const locations2GeojsonTrajectory = (trip, locationList, trajectoryColor?) => { } else { // this is a multimodal trip so we sort the locations into sections by timestamp sectionsPoints = trip.sections.map((s) => - trip.locations.filter((l) => l.ts >= s.start_ts && l.ts <= s.end_ts), + trip.locations.filter((l) => + l.ts >= s.start_ts && l.ts <= s.end_ts + ) ); } return sectionsPoints.map((sectionPoints, i) => { const section = trip.sections?.[i]; return { - type: 'Feature', + type: "Feature", geometry: { - type: 'LineString', + type: "LineString", coordinates: sectionPoints.map((pt) => pt.loc.coordinates), }, style: { /* If a color was passed as arg, use it for the whole trajectory. Otherwise, use the color for the sensed mode of this section, and fall back to dark grey */ - color: trajectoryColor || getBaseModeByKey(section?.sensed_mode_str)?.color || '#333', + color: trajectoryColor || getBaseModeByKey(section?.sensed_mode_str)?.color || "#333", }, - }; + } }); -}; +} diff --git a/www/js/diary/useDerivedProperties.tsx b/www/js/diary/useDerivedProperties.tsx index fe324ee3f..604fef227 100644 --- a/www/js/diary/useDerivedProperties.tsx +++ b/www/js/diary/useDerivedProperties.tsx @@ -1,16 +1,9 @@ -import { useMemo } from 'react'; -import { useImperialConfig } from '../config/useImperialConfig'; -import { - getFormattedDate, - getFormattedDateAbbr, - getFormattedSectionProperties, - getFormattedTimeRange, - getLocalTimeString, - getDetectedModes, - isMultiDay, -} from './diaryHelper'; +import { useMemo } from "react"; +import { useImperialConfig } from "../config/useImperialConfig"; +import { getFormattedDate, getFormattedDateAbbr, getFormattedSectionProperties, getFormattedTimeRange, getLocalTimeString, getDetectedModes, isMultiDay } from "./diaryHelper"; const useDerivedProperties = (tlEntry) => { + const imperialConfig = useImperialConfig(); return useMemo(() => { @@ -19,7 +12,7 @@ const useDerivedProperties = (tlEntry) => { const beginDt = tlEntry.start_local_dt || tlEntry.enter_local_dt; const endDt = tlEntry.end_local_dt || tlEntry.exit_local_dt; const tlEntryIsMultiDay = isMultiDay(beginFmt, endFmt); - + return { displayDate: getFormattedDate(beginFmt, endFmt), displayStartTime: getLocalTimeString(beginDt), @@ -31,8 +24,8 @@ const useDerivedProperties = (tlEntry) => { formattedSectionProperties: getFormattedSectionProperties(tlEntry, imperialConfig), distanceSuffix: imperialConfig.distanceSuffix, detectedModes: getDetectedModes(tlEntry), - }; + } }, [tlEntry, imperialConfig]); -}; +} export default useDerivedProperties; diff --git a/www/js/i18n-utils.js b/www/js/i18n-utils.js index bcfb74391..45cca7043 100644 --- a/www/js/i18n-utils.js +++ b/www/js/i18n-utils.js @@ -2,48 +2,39 @@ import angular from 'angular'; -angular.module('emission.i18n.utils', []).factory('i18nUtils', function ($http, Logger) { +angular.module('emission.i18n.utils', []) +.factory("i18nUtils", function($http, Logger) { var iu = {}; // copy-pasted from ngCordova, and updated to promises - iu.checkFile = function (fn) { - return new Promise(function (resolve, reject) { - if (/^\//.test(fn)) { - reject('directory cannot start with /'); + iu.checkFile = function(fn) { + return new Promise(function(resolve, reject) { + if ((/^\//.test(fn))) { + reject('directory cannot start with \/'); } return $http.get(fn); }); - }; + } // The language comes in between the first and second part // the default path should end with a "/" iu.geti18nFileName = function (defaultPath, fpFirstPart, fpSecondPart) { const lang = i18next.resolvedLanguage; - const i18nPath = 'i18n/'; + const i18nPath = "i18n/"; var defaultVal = defaultPath + fpFirstPart + fpSecondPart; if (lang != 'en') { - var url = i18nPath + fpFirstPart + '-' + lang + fpSecondPart; - return $http - .get(url) - .then(function (result) { - Logger.log( - window.Logger.LEVEL_DEBUG, - 'Successfully found the ' + - url + - ', result is ' + - JSON.stringify(result.data).substring(0, 10), - ); - return url; - }) - .catch(function (err) { - Logger.log( - window.Logger.LEVEL_DEBUG, - url + ' file not found, loading english version, error is ' + JSON.stringify(err), - ); - return Promise.resolve(defaultVal); - }); + var url = i18nPath + fpFirstPart + "-" + lang + fpSecondPart; + return $http.get(url).then( function(result){ + Logger.log(window.Logger.LEVEL_DEBUG, + "Successfully found the "+url+", result is " + JSON.stringify(result.data).substring(0,10)); + return url; + }).catch(function (err) { + Logger.log(window.Logger.LEVEL_DEBUG, + url+" file not found, loading english version, error is " + JSON.stringify(err)); + return Promise.resolve(defaultVal); + }); } return Promise.resolve(defaultVal); - }; + } return iu; }); diff --git a/www/js/i18nextInit.ts b/www/js/i18nextInit.ts index 88bcb51be..48177caf5 100644 --- a/www/js/i18nextInit.ts +++ b/www/js/i18nextInit.ts @@ -21,7 +21,7 @@ const mergeInTranslations = (lang, fallbackLang) => { console.warn(`Missing translation for key '${key}'`); if (__DEV__) { if (typeof value === 'string') { - lang[key] = `🌐${value}`; + lang[key] = `🌐${value}` } else if (typeof value === 'object') { lang[key] = {}; mergeInTranslations(lang[key], value); @@ -30,11 +30,11 @@ const mergeInTranslations = (lang, fallbackLang) => { lang[key] = value; } } else if (typeof value === 'object') { - mergeInTranslations(lang[key], fallbackLang[key]); + mergeInTranslations(lang[key], fallbackLang[key]) } }); return lang; -}; +} import enJson from '../i18n/en.json'; import esJson from '../../locales/es/i18n/es.json'; @@ -59,24 +59,22 @@ for (const locale of locales) { } } -i18next.use(initReactI18next).init({ - debug: true, - resources: langs, - lng: detectedLang, - fallbackLng: 'en', -}); +i18next.use(initReactI18next) + .init({ + debug: true, + resources: langs, + lng: detectedLang, + fallbackLng: 'en' + }); export default i18next; // Next, register the translations for react-native-paper-dates import { en, es, fr, it, registerTranslation } from 'react-native-paper-dates'; const rnpDatesLangs = { - en, - es, - fr, - it, + en, es, fr, it, lo: loJson['react-native-paper-dates'] /* Lao translations are not included in the library, - so we register them from 'lo.json' in /locales */, + so we register them from 'lo.json' in /locales */ }; for (const lang of Object.keys(rnpDatesLangs)) { registerTranslation(lang, rnpDatesLangs[lang]); diff --git a/www/js/intro.js b/www/js/intro.js index 41e05a59d..0bc7fa01d 100644 --- a/www/js/intro.js +++ b/www/js/intro.js @@ -3,274 +3,241 @@ import angular from 'angular'; import QrCode from './components/QrCode'; -angular - .module('emission.intro', [ - 'emission.splash.startprefs', - 'emission.survey.enketo.demographics', - 'emission.appstatus.permissioncheck', - 'emission.i18n.utils', - 'emission.config.dynamic', - 'emission.plugin.kvstore', - 'ionic-toast', - QrCode.module, - ]) - - .config(function ($stateProvider) { - $stateProvider - // setup an abstract state for the intro directive - .state('root.intro', { - url: '/intro', - templateUrl: 'templates/intro/intro.html', - controller: 'IntroCtrl', - }) - .state('root.reconsent', { - url: '/reconsent', - templateUrl: 'templates/intro/reconsent.html', - controller: 'IntroCtrl', - }); +angular.module('emission.intro', ['emission.splash.startprefs', + 'emission.survey.enketo.demographics', + 'emission.appstatus.permissioncheck', + 'emission.i18n.utils', + 'emission.config.dynamic', + 'emission.plugin.kvstore', + 'ionic-toast', + QrCode.module]) + +.config(function($stateProvider) { + $stateProvider + // setup an abstract state for the intro directive + .state('root.intro', { + url: '/intro', + templateUrl: 'templates/intro/intro.html', + controller: 'IntroCtrl' }) - - .controller( - 'IntroCtrl', - function ( - $scope, - $rootScope, - $state, - $window, - $ionicPlatform, - $ionicSlideBoxDelegate, - $ionicPopup, - $ionicHistory, - ionicToast, - $timeout, - CommHelper, - StartPrefs, - KVStore, - SurveyLaunch, - DynamicConfig, - i18nUtils, - ) { - /* - * Move all the state that is currently in the controller body into the init - * function so that we can reload if we need to - */ - $scope.init = function () { - var allIntroFiles = Promise.all([ - i18nUtils.geti18nFileName('templates/', 'intro/summary', '.html'), - i18nUtils.geti18nFileName('templates/', 'intro/consent', '.html'), - i18nUtils.geti18nFileName('templates/', 'intro/sensor_explanation', '.html'), - i18nUtils.geti18nFileName('templates/', 'intro/survey', '.html'), - ]); - allIntroFiles.then(function (allIntroFilePaths) { - $scope.$apply(function () { - console.log('intro files are ' + allIntroFilePaths); - $scope.summaryFile = allIntroFilePaths[0]; - $scope.consentFile = allIntroFilePaths[1]; - $scope.explainFile = allIntroFilePaths[2]; - $scope.surveyFile = allIntroFilePaths[3]; - }); + .state('root.reconsent', { + url: '/reconsent', + templateUrl: 'templates/intro/reconsent.html', + controller: 'IntroCtrl' + }); +}) + +.controller('IntroCtrl', function($scope, $rootScope, $state, $window, + $ionicPlatform, $ionicSlideBoxDelegate, + $ionicPopup, $ionicHistory, ionicToast, $timeout, CommHelper, StartPrefs, KVStore, SurveyLaunch, DynamicConfig, i18nUtils) { + + /* + * Move all the state that is currently in the controller body into the init + * function so that we can reload if we need to + */ + $scope.init = function() { + var allIntroFiles = Promise.all([ + i18nUtils.geti18nFileName("templates/", "intro/summary", ".html"), + i18nUtils.geti18nFileName("templates/", "intro/consent", ".html"), + i18nUtils.geti18nFileName("templates/", "intro/sensor_explanation", ".html"), + i18nUtils.geti18nFileName("templates/", "intro/survey", ".html") + ]); + allIntroFiles.then(function(allIntroFilePaths) { + $scope.$apply(function() { + console.log("intro files are "+allIntroFilePaths); + $scope.summaryFile = allIntroFilePaths[0]; + $scope.consentFile = allIntroFilePaths[1]; + $scope.explainFile = allIntroFilePaths[2]; + $scope.surveyFile = allIntroFilePaths[3]; }); - }; - - $scope.getIntroBox = function () { - return $ionicSlideBoxDelegate.$getByHandle('intro-box'); - }; - - $scope.stopSliding = function () { - $scope.getIntroBox().enableSlide(false); - }; - - $scope.showSettings = function () { - window.cordova.plugins.BEMConnectionSettings.getSettings().then( - function (settings) { - var errorMsg = JSON.stringify(settings); - var alertPopup = $ionicPopup.alert({ - title: 'settings', - template: errorMsg, - }); - - alertPopup.then(function (res) { - $scope.next(); - }); - }, - function (error) { - $scope.alertError('getting settings', error); - }, - ); - }; - - $scope.overallStatus = false; - - /* If the user does not consent, we boot them back out to the join screen */ - $scope.disagree = function () { - // reset the saved config, then trigger a hard refresh - DynamicConfig.resetConfigAndRefresh(); - }; + }); + } + + $scope.getIntroBox = function() { + return $ionicSlideBoxDelegate.$getByHandle('intro-box'); + }; + + $scope.stopSliding = function() { + $scope.getIntroBox().enableSlide(false); + }; + + $scope.showSettings = function() { + window.cordova.plugins.BEMConnectionSettings.getSettings().then(function(settings) { + var errorMsg = JSON.stringify(settings); + var alertPopup = $ionicPopup.alert({ + title: 'settings', + template: errorMsg + }); - $scope.agree = function () { - $scope.scannedToken = $scope.ui_config.joined.opcode; - StartPrefs.markConsented().then(function (response) { - $ionicHistory.clearHistory(); - if ($scope.scannedToken) { - $scope.login($scope.scannedToken); + alertPopup.then(function(res) { + $scope.next(); + }); + }, function(error) { + $scope.alertError('getting settings', error); + }); + }; + + $scope.overallStatus = false; + + /* If the user does not consent, we boot them back out to the join screen */ + $scope.disagree = function() { + // reset the saved config, then trigger a hard refresh + DynamicConfig.resetConfigAndRefresh(); + }; + + $scope.agree = function() { + $scope.scannedToken = $scope.ui_config.joined.opcode; + StartPrefs.markConsented().then(function(response) { + $ionicHistory.clearHistory(); + if ($scope.scannedToken) { + $scope.login($scope.scannedToken); + } else { + if ($state.is('root.intro')) { + $scope.next(); } else { - if ($state.is('root.intro')) { - $scope.next(); - } else { - StartPrefs.loadPreferredScreen(); - } + StartPrefs.loadPreferredScreen(); } - }); - }; - - $scope.next = function () { - $scope.getIntroBox().next(); - }; - - $scope.previous = function () { - $scope.getIntroBox().previous(); - }; - - $scope.alertError = function (title, errorResult) { - var errorMsg = JSON.stringify(errorResult); - var alertPopup = $ionicPopup.alert({ - title: title, - template: errorMsg, - }); + } + }); + }; + + $scope.next = function() { + $scope.getIntroBox().next(); + }; + + $scope.previous = function() { + $scope.getIntroBox().previous(); + }; + + $scope.alertError = function(title, errorResult) { + var errorMsg = JSON.stringify(errorResult); + var alertPopup = $ionicPopup.alert({ + title: title, + template: errorMsg + }); - alertPopup.then(function (res) { - window.Logger.log(window.Logger.LEVEL_INFO, errorMsg + ' ' + res); + alertPopup.then(function(res) { + window.Logger.log(window.Logger.LEVEL_INFO, errorMsg + ' ' + res); + }); + } + + $scope.login = function(token) { + const EXPECTED_METHOD = "prompted-auth"; + const dbStorageObject = {"token": token}; + KVStore.set(EXPECTED_METHOD, dbStorageObject).then(function(opcode) { + // ionicToast.show(message, position, stick, time); + // $scope.next(); + ionicToast.show(opcode, 'middle', false, 2500); + if (opcode == "null" || opcode == "") { + $scope.alertError("Invalid login "+opcode); + } else { + CommHelper.registerUser(function(successResult) { + $scope.currentToken = token; + $scope.qrToken = "emission://login_token?token=" + token; + $scope.next(); + }, function(errorResult) { + $scope.alertError('User registration error', errorResult); }); - }; + } + }, function(error) { + $scope.alertError('Sign in error', error); + }); - $scope.login = function (token) { - const EXPECTED_METHOD = 'prompted-auth'; - const dbStorageObject = { token: token }; - KVStore.set(EXPECTED_METHOD, dbStorageObject).then( - function (opcode) { - // ionicToast.show(message, position, stick, time); - // $scope.next(); - ionicToast.show(opcode, 'middle', false, 2500); - if (opcode == 'null' || opcode == '') { - $scope.alertError('Invalid login ' + opcode); - } else { - CommHelper.registerUser( - function (successResult) { - $scope.currentToken = token; - $scope.qrToken = 'emission://login_token?token=' + token; - $scope.next(); - }, - function (errorResult) { - $scope.alertError('User registration error', errorResult); - }, - ); - } - }, - function (error) { - $scope.alertError('Sign in error', error); - }, - ); - }; + }; - $scope.shareQR = function () { - /*code adapted from demo of react-qr-code + $scope.shareQR = function() { + /*code adapted from demo of react-qr-code selector below gets svg element out of angularized QRCode this will change upon later migration*/ - const svg = document.querySelector('qr-code svg'); - const svgData = new XMLSerializer().serializeToString(svg); - const img = new Image(); - - img.onload = () => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - const pngFile = canvas.toDataURL('image/png'); - - var prepopulateQRMessage = {}; - prepopulateQRMessage.files = [pngFile]; - prepopulateQRMessage.url = $scope.currentToken; - prepopulateQRMessage.message = $scope.currentToken; - - 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); - }, - ); - }; - img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; - }; - - // Called each time the slide changes - $scope.slideChanged = function (index) { - $scope.slideIndex = index; - /* - * The slidebox is created as a child of the HTML page that this controller - * is associated with, so it is not available when the controller is created. - * There is an onLoad, but it is for ng-include, not for random divs, apparently. - * Trying to create a new controller complains because then both the - * directive and the controller are trying to ask for a new scope. - * So instead, I turn off swiping after the initial summary is past. - * Since the summary is not legally binding, it is fine to swipe past it... - */ - if (index > 0) { - $scope.getIntroBox().enableSlide(false); - } - }; - - $scope.finish = function () { - // this is not a promise, so we don't need to use .then - StartPrefs.markIntroDone(); - $scope.getIntroBox().slide(0); - StartPrefs.loadPreferredScreen(); - // remove this view since the intro is done - // when we go back to the intro state, it will be recreated - $("[state='root.intro']").remove(); - $scope.$destroy(); - }; - - $ionicPlatform.ready().then(function () { - console.log('app is launched, currently NOP'); + const svg = document.querySelector("qr-code svg"); + const svgData = new XMLSerializer().serializeToString(svg); + const img = new Image(); + + img.onload = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const pngFile = canvas.toDataURL("image/png"); + + var prepopulateQRMessage = {}; + prepopulateQRMessage.files = [pngFile]; + prepopulateQRMessage.url = $scope.currentToken; + prepopulateQRMessage.message = $scope.currentToken; + + 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); }); - - $ionicPlatform.ready().then(() => { - DynamicConfig.configReady().then((newConfig) => { - Logger.log('Resolved UI_CONFIG_READY promise in intro.js, filling in templates'); - $scope.lang = i18next.resolvedLanguage; - $scope.ui_config = newConfig; - - // backwards compat hack to fill in the raw_data_use for programs that don't have it - const default_raw_data_use = { - en: `to monitor the ${newConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, - es: `para monitorear el ${newConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes`, - }; - Object.entries(newConfig.intro.translated_text).forEach(([lang, val]) => { + } + img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; + } + + // Called each time the slide changes + $scope.slideChanged = function(index) { + $scope.slideIndex = index; + /* + * The slidebox is created as a child of the HTML page that this controller + * is associated with, so it is not available when the controller is created. + * There is an onLoad, but it is for ng-include, not for random divs, apparently. + * Trying to create a new controller complains because then both the + * directive and the controller are trying to ask for a new scope. + * So instead, I turn off swiping after the initial summary is past. + * Since the summary is not legally binding, it is fine to swipe past it... + */ + if (index > 0) { + $scope.getIntroBox().enableSlide(false); + } + }; + + $scope.finish = function() { + // this is not a promise, so we don't need to use .then + StartPrefs.markIntroDone(); + $scope.getIntroBox().slide(0); + StartPrefs.loadPreferredScreen(); + // remove this view since the intro is done + // when we go back to the intro state, it will be recreated + $("[state='root.intro']").remove(); + $scope.$destroy(); + } + + $ionicPlatform.ready().then(function() { + console.log("app is launched, currently NOP"); + }); + + $ionicPlatform.ready().then(() => { + DynamicConfig.configReady().then((newConfig) => { + Logger.log("Resolved UI_CONFIG_READY promise in intro.js, filling in templates"); + $scope.lang = i18next.resolvedLanguage; + $scope.ui_config = newConfig; + + // backwards compat hack to fill in the raw_data_use for programs that don't have it + const default_raw_data_use = { + "en": `to monitor the ${newConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, + "es": `para monitorear el ${newConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes` + } + Object.entries(newConfig.intro.translated_text).forEach(([lang, val]) => { val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; - }); - // TODO: we should be able to use i18n for this, right? - $scope.template_text = newConfig.intro.translated_text[$scope.lang]; - if (!$scope.template_text) { - $scope.template_text = newConfig.intro.translated_text['en']; - } - // Backwards compat hack to fill in the `app_required` based on the - // old-style "program_or_study" - // remove this at the end of 2023 when all programs have been migrated over - if ($scope.ui_config.intro.app_required == undefined) { - $scope.ui_config.intro.app_required = - $scope.ui_config?.intro.program_or_study == 'program'; - } - $scope.ui_config.opcode = $scope.ui_config.opcode || {}; - if ($scope.ui_config.opcode.autogen == undefined) { - $scope.ui_config.opcode.autogen = $scope.ui_config?.intro.program_or_study == 'study'; - } - $scope.init(); }); + // TODO: we should be able to use i18n for this, right? + $scope.template_text = newConfig.intro.translated_text[$scope.lang]; + if (!$scope.template_text) { + $scope.template_text = newConfig.intro.translated_text["en"] + } + // Backwards compat hack to fill in the `app_required` based on the + // old-style "program_or_study" + // remove this at the end of 2023 when all programs have been migrated over + if ($scope.ui_config.intro.app_required == undefined) { + $scope.ui_config.intro.app_required = $scope.ui_config?.intro.program_or_study == 'program'; + } + $scope.ui_config.opcode = $scope.ui_config.opcode || {}; + if ($scope.ui_config.opcode.autogen == undefined) { + $scope.ui_config.opcode.autogen = $scope.ui_config?.intro.program_or_study == 'study'; + } + $scope.init(); }); - }, - ); + }); +}); diff --git a/www/js/join/join-ctrl.js b/www/js/join/join-ctrl.js index 13faf723e..85d6424c1 100644 --- a/www/js/join/join-ctrl.js +++ b/www/js/join/join-ctrl.js @@ -1,79 +1,70 @@ import angular from 'angular'; -angular - .module('emission.join.ctrl', [ - 'emission.splash.startprefs', - 'emission.splash.pushnotify', - 'emission.splash.storedevicesettings', - 'emission.splash.localnotify', - 'emission.splash.remotenotify', - 'emission.stats.clientstats', - ]) - .controller( - 'JoinCtrl', - function ($scope, $state, $interval, $rootScope, $ionicPlatform, $ionicPopup, $ionicPopover) { - console.log('JoinCtrl invoked'); - // alert("attach debugger!"); - // PushNotify.startupInit(); - console.log('JoinCtrl invoke finished'); +angular.module('emission.join.ctrl', ['emission.splash.startprefs', + 'emission.splash.pushnotify', + 'emission.splash.storedevicesettings', + 'emission.splash.localnotify', + 'emission.splash.remotenotify', + 'emission.stats.clientstats']) +.controller('JoinCtrl', function($scope, $state, $interval, $rootScope, + $ionicPlatform, $ionicPopup, $ionicPopover) { + console.log('JoinCtrl invoked'); + // alert("attach debugger!"); + // PushNotify.startupInit(); + console.log('JoinCtrl invoke finished'); - $ionicPlatform.ready(function () { - $scope.scanEnabled = true; - }); + $ionicPlatform.ready(function() { + $scope.scanEnabled = true; + }); - $ionicPopover - .fromTemplateUrl('templates/join/about-app.html', { - backdropClickToClose: true, - hardwareBackButtonClose: true, - scope: $scope, - }) - .then(function (popover) { - $scope.popover = popover; - $scope.isIOS = $ionicPlatform.is('ios'); - $scope.isAndroid = $ionicPlatform.is('android'); - }); + $ionicPopover.fromTemplateUrl('templates/join/about-app.html', { + backdropClickToClose: true, + hardwareBackButtonClose: true, + scope: $scope + }).then(function(popover) { + $scope.popover = popover; + $scope.isIOS = $ionicPlatform.is('ios'); + $scope.isAndroid = $ionicPlatform.is('android'); + }); - $scope.showDetails = function ($event) { - $scope.popover.show($event); - }; + $scope.showDetails = function($event) { + $scope.popover.show($event) + } - $scope.hideDetails = function ($event) { - $scope.popover.hide($event); - }; + $scope.hideDetails = function($event) { + $scope.popover.hide($event) + } - function handleOpenURL(url) { - console.log('onLaunch method from external function called'); - var c = document.querySelectorAll('[ng-app]')[0]; - var scope = angular.element(c).scope(); - scope.$broadcast('CUSTOM_URL_LAUNCH', url); - } + function handleOpenURL(url) { + console.log("onLaunch method from external function called"); + var c = document.querySelectorAll("[ng-app]")[0]; + var scope = angular.element(c).scope(); + scope.$broadcast("CUSTOM_URL_LAUNCH", url); + }; - $scope.scanCode = function () { - if (!$scope.scanEnabled) { - $ionicPopup.alert({ template: 'plugins not yet initialized, please retry later' }); - } else { - cordova.plugins.barcodeScanner.scan( - function (result) { - if ( - result.format == 'QR_CODE' && - result.cancelled == false && - result.text.startsWith('emission://') - ) { - handleOpenURL(result.text); + $scope.scanCode = function() { + if (!$scope.scanEnabled) { + $ionicPopup.alert({template: "plugins not yet initialized, please retry later"}); + } else { + cordova.plugins.barcodeScanner.scan( + function (result) { + if (result.format == "QR_CODE" && + result.cancelled == false && + result.text.startsWith("emission://")) { + handleOpenURL(result.text); } else { - $ionicPopup.alert({ template: 'invalid study reference ' + result.text }); + $ionicPopup.alert({template: "invalid study reference "+result.text}); } - }, - function (error) { - $ionicPopup.alert({ template: 'Scanning failed: ' + error }); - }, - ); - } - }; // scanCode + }, + function (error) { + $ionicPopup.alert({template: "Scanning failed: " + error}); + }); + } + }; // scanCode - $scope.pasteCode = function () { - $scope.data = {}; - const tokenPopup = $ionicPopup.show({ + $scope.pasteCode = function() { + $scope.data = {}; + const tokenPopup = $ionicPopup.show({ template: ``, title: i18next.t('login.enter-existing-token') + '
', @@ -82,7 +73,7 @@ angular { text: '' + i18next.t('login.button-accept') + '', type: 'button-positive', - onTap: function (e) { + onTap: function(e) { if (!$scope.data.existing_token) { //don't allow the user to close unless he enters a username @@ -90,26 +81,22 @@ angular } else { return $scope.data.existing_token; } - }, - }, - { + } + },{ text: '' + i18next.t('login.button-decline') + '', type: 'button-stable', - onTap: function (e) { + onTap: function(e) { return null; - }, - }, - ], - }); - tokenPopup - .then(function (token) { - if (token != null) { - handleOpenURL('emission://login_token?token=' + token); + } } - }) - .catch(function (err) { - $scope.alertError(err); - }); - }; - }, - ); + ] + }); + tokenPopup.then(function(token) { + if (token != null) { + handleOpenURL("emission://login_token?token="+token); + } + }).catch(function(err) { + $scope.alertError(err); + }); + }; +}); diff --git a/www/js/main.js b/www/js/main.js index 67cef39a5..6e68d16b3 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -4,111 +4,93 @@ import angular from 'angular'; import MetricsTab from './metrics/MetricsTab'; -angular - .module('emission.main', [ - 'emission.main.diary', - 'emission.main.control', - 'emission.main.metrics.factory', - 'emission.main.metrics.mappings', - 'emission.config.dynamic', - 'emission.services', - 'emission.services.upload', - MetricsTab.module, - ]) +angular.module('emission.main', ['emission.main.diary', + 'emission.main.control', + 'emission.main.metrics.factory', + 'emission.main.metrics.mappings', + 'emission.config.dynamic', + 'emission.services', + 'emission.services.upload', + MetricsTab.module]) - .config(function ($stateProvider, $ionicConfigProvider, $urlRouterProvider) { - $stateProvider - // setup an abstract state for the tabs directive - .state('root.main', { - url: '/main', - abstract: true, - templateUrl: 'templates/main.html', - controller: 'MainCtrl', - }) - - .state('root.main.metrics', { - url: '/metrics', - views: { - 'main-metrics': { - template: ``, - }, - }, - }) - - .state('root.main.control', { - url: '/control', - params: { - launchAppStatusModal: false, - }, - views: { - 'main-control': { - template: ``, - controller: 'ControlCtrl', - }, - }, - }); +.config(function($stateProvider, $ionicConfigProvider, $urlRouterProvider) { + $stateProvider + // setup an abstract state for the tabs directive + .state('root.main', { + url: '/main', + abstract: true, + templateUrl: 'templates/main.html', + controller: 'MainCtrl' + }) - $ionicConfigProvider.tabs.style('standard'); - $ionicConfigProvider.tabs.position('bottom'); + .state('root.main.metrics', { + url: '/metrics', + views: { + 'main-metrics': { + template: ``, + } + } }) - .controller('appCtrl', function ($scope, $ionicModal, $timeout) { - $scope.openNativeSettings = function () { - window.Logger.log(window.Logger.LEVEL_DEBUG, 'about to open native settings'); - window.cordova.plugins.BEMLaunchNative.launch( - 'NativeSettings', - function (result) { - window.Logger.log( - window.Logger.LEVEL_DEBUG, - 'Successfully opened screen NativeSettings, result is ' + result, - ); - }, - function (err) { - window.Logger.log( - window.Logger.LEVEL_ERROR, - 'Unable to open screen NativeSettings because of err ' + err, - ); - }, - ); - }; + .state('root.main.control', { + url: '/control', + params: { + launchAppStatusModal: false + }, + views: { + 'main-control': { + template: ``, + controller: 'ControlCtrl' + } + } }) - .controller('MainCtrl', function ($scope, $state, $rootScope, $ionicPlatform, DynamicConfig) { + $ionicConfigProvider.tabs.style('standard') + $ionicConfigProvider.tabs.position('bottom'); +}) + +.controller('appCtrl', function($scope, $ionicModal, $timeout) { + $scope.openNativeSettings = function() { + window.Logger.log(window.Logger.LEVEL_DEBUG, "about to open native settings"); + window.cordova.plugins.BEMLaunchNative.launch("NativeSettings", function(result) { + window.Logger.log(window.Logger.LEVEL_DEBUG, + "Successfully opened screen NativeSettings, result is "+result); + }, function(err) { + window.Logger.log(window.Logger.LEVEL_ERROR, + "Unable to open screen NativeSettings because of err "+err); + }); + } +}) + +.controller('MainCtrl', function($scope, $state, $rootScope, $ionicPlatform, DynamicConfig) { // Currently this is blank since it is basically a placeholder for the // three screens. But we can totally add hooks here if we want. It is the // controller for all the screens because none of them do anything for now. moment.locale(i18next.resolvedLanguage); - $scope.tabsCustomClass = function () { - return 'tabs-icon-top tabs-custom'; - }; + $scope.tabsCustomClass = function() { + return "tabs-icon-top tabs-custom"; + } - $ionicPlatform.ready().then(function () { + $ionicPlatform.ready().then(function() { DynamicConfig.configReady().then((newConfig) => { $scope.dCfg = newConfig; $scope.showMetrics = newConfig.survey_info['trip-labels'] == 'MULTILABEL'; - console.log('screen-select: showMetrics = ' + $scope.showMetrics); - console.log('screen-select: in dynamic config load, tabs list is ', $('.tab-item')); + console.log("screen-select: showMetrics = "+$scope.showMetrics);; + console.log("screen-select: in dynamic config load, tabs list is ", $('.tab-item')); }); }); - $scope.$on('$ionicView.enter', function (ev) { - console.log('screen-select: after view enter, tabs list is ', $('.tab-item')); - const labelEl = $('.tab-item[icon="ion-checkmark-round"]'); - const diaryEl = $('.tab-item[icon="ion-map"]'); - const dashboardEl = $('.tab-item[icon="ion-ios-analytics"]'); - console.log( - 'screen-select: label ', - labelEl, - ' diary ', - diaryEl, - ' dashboardEl', - dashboardEl, - ); - // If either these don't exist, we will get an empty array. - // preceding or succeeding with an empty array is a NOP - labelEl.before(diaryEl); - labelEl.after(dashboardEl); + $scope.$on('$ionicView.enter', function(ev) { + console.log("screen-select: after view enter, tabs list is ", $('.tab-item')); + const labelEl = $('.tab-item[icon="ion-checkmark-round"]') + const diaryEl = $('.tab-item[icon="ion-map"]') + const dashboardEl = $('.tab-item[icon="ion-ios-analytics"]') + console.log("screen-select: label ",labelEl," diary ",diaryEl," dashboardEl" ,dashboardEl); + // If either these don't exist, we will get an empty array. + // preceding or succeeding with an empty array is a NOP + labelEl.before(diaryEl); + labelEl.after(dashboardEl); }); - }); +}); diff --git a/www/js/metrics-factory.js b/www/js/metrics-factory.js index ae087a676..ce813fbaa 100644 --- a/www/js/metrics-factory.js +++ b/www/js/metrics-factory.js @@ -1,273 +1,238 @@ 'use strict'; import angular from 'angular'; -import { getBaseModeByValue } from './diary/diaryHelper'; +import { getBaseModeByValue } from './diary/diaryHelper' import { labelOptions } from './survey/multilabel/confirmHelper'; -angular - .module('emission.main.metrics.factory', [ - 'emission.main.metrics.mappings', - 'emission.plugin.kvstore', - ]) +angular.module('emission.main.metrics.factory', + ['emission.main.metrics.mappings', + 'emission.plugin.kvstore']) - .factory('FootprintHelper', function (CarbonDatasetHelper, CustomDatasetHelper) { - var fh = {}; - var highestFootprint = 0; +.factory('FootprintHelper', function(CarbonDatasetHelper, CustomDatasetHelper) { + var fh = {}; + var highestFootprint = 0; - var mtokm = function (v) { - return v / 1000; - }; - fh.useCustom = false; + var mtokm = function(v) { + return v / 1000; + } + fh.useCustom = false; - fh.setUseCustomFootprint = function () { - fh.useCustom = true; - }; + fh.setUseCustomFootprint = function () { + fh.useCustom = true; + } - fh.getFootprint = function () { - if (this.useCustom == true) { + fh.getFootprint = function() { + if (this.useCustom == true) { return CustomDatasetHelper.getCustomFootprint(); - } else { + } else { return CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); + } + } + + fh.readableFormat = function(v) { + return v > 999? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; + } + fh.getFootprintForMetrics = function(userMetrics, defaultIfMissing=0) { + var footprint = fh.getFootprint(); + var result = 0; + for (var i in userMetrics) { + var mode = userMetrics[i].key; + if (mode == 'ON_FOOT') { + mode = 'WALKING'; } - }; - fh.readableFormat = function (v) { - return v > 999 ? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; - }; - fh.getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { - var footprint = fh.getFootprint(); - var result = 0; - for (var i in userMetrics) { - var mode = userMetrics[i].key; - if (mode == 'ON_FOOT') { - mode = 'WALKING'; - } - - if (mode in footprint) { - result += footprint[mode] * mtokm(userMetrics[i].values); - } else if (mode == 'IN_VEHICLE') { - result += - ((footprint['CAR'] + - footprint['BUS'] + - footprint['LIGHT_RAIL'] + - footprint['TRAIN'] + - footprint['TRAM'] + - footprint['SUBWAY']) / - 6) * - mtokm(userMetrics[i].values); - } else { - console.warn( - 'WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + - mode + - ' metrics JSON: ' + - JSON.stringify(userMetrics), - ); - result += defaultIfMissing * mtokm(userMetrics[i].values); - } + if (mode in footprint) { + result += footprint[mode] * mtokm(userMetrics[i].values); + } else if (mode == 'IN_VEHICLE') { + result += ((footprint['CAR'] + footprint['BUS'] + footprint["LIGHT_RAIL"] + footprint['TRAIN'] + footprint['TRAM'] + footprint['SUBWAY']) / 6) * mtokm(userMetrics[i].values); + } else { + console.warn('WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + mode + " metrics JSON: " + JSON.stringify(userMetrics)); + result += defaultIfMissing * mtokm(userMetrics[i].values); } - return result; - }; - fh.getLowestFootprintForDistance = function (distance) { - var footprint = fh.getFootprint(); - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'WALKING' || mode == 'BICYCLING') { - // these modes aren't considered when determining the lowest carbon footprint - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } + } + return result; + } + fh.getLowestFootprintForDistance = function(distance) { + var footprint = fh.getFootprint(); + var lowestFootprint = Number.MAX_SAFE_INTEGER; + for (var mode in footprint) { + if (mode == 'WALKING' || mode == 'BICYCLING') { + // these modes aren't considered when determining the lowest carbon footprint + } + else { + lowestFootprint = Math.min(lowestFootprint, footprint[mode]); } - return lowestFootprint * mtokm(distance); - }; + } + return lowestFootprint * mtokm(distance); + } - fh.getHighestFootprint = function () { - if (!highestFootprint) { + fh.getHighestFootprint = function() { + if (!highestFootprint) { var footprint = fh.getFootprint(); let footprintList = []; for (var mode in footprint) { - footprintList.push(footprint[mode]); + footprintList.push(footprint[mode]); } highestFootprint = Math.max(...footprintList); + } + return highestFootprint; + } + + fh.getHighestFootprintForDistance = function(distance) { + return fh.getHighestFootprint() * mtokm(distance); + } + + var getLowestMotorizedNonAirFootprint = function(footprint, rlmCO2) { + var lowestFootprint = Number.MAX_SAFE_INTEGER; + for (var mode in footprint) { + if (mode == 'AIR_OR_HSR' || mode == 'air') { + console.log("Air mode, ignoring"); } - return highestFootprint; - }; - - fh.getHighestFootprintForDistance = function (distance) { - return fh.getHighestFootprint() * mtokm(distance); - }; - - var getLowestMotorizedNonAirFootprint = function (footprint, rlmCO2) { - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'AIR_OR_HSR' || mode == 'air') { - console.log('Air mode, ignoring'); + else { + if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { + console.log("Non motorized mode or footprint <= range_limited_motorized", mode, footprint[mode], rlmCO2); } else { - if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { - console.log( - 'Non motorized mode or footprint <= range_limited_motorized', - mode, - footprint[mode], - rlmCO2, - ); - } else { lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } } } - return lowestFootprint; - }; - - fh.getOptimalDistanceRanges = function () { - const FIVE_KM = 5 * 1000; - const SIX_HUNDRED_KM = 600 * 1000; - if (!fh.useCustom) { + } + return lowestFootprint; + } + + fh.getOptimalDistanceRanges = function() { + const FIVE_KM = 5 * 1000; + const SIX_HUNDRED_KM = 600 * 1000; + if (!fh.useCustom) { const defaultFootprint = CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(defaultFootprint); - const airFootprint = defaultFootprint['AIR_OR_HSR']; + const airFootprint = defaultFootprint["AIR_OR_HSR"]; return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { + {low: 0, high: FIVE_KM, optimal: 0}, + {low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir}, + {low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint}]; + } else { // custom footprint, let's get the custom values const customFootprint = CustomDatasetHelper.getCustomFootprint(); - let airFootprint = customFootprint['air']; + let airFootprint = customFootprint["air"] if (!airFootprint) { - // 2341 BTU/PMT from - // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 - // 159.25 lb per million BTU from EIA - // https://www.eia.gov/environment/emissions/co2_vol_mass.php - // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit - console.log('No entry for air in ', customFootprint, ' using default'); - airFootprint = 0.1; + // 2341 BTU/PMT from + // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 + // 159.25 lb per million BTU from EIA + // https://www.eia.gov/environment/emissions/co2_vol_mass.php + // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit + console.log("No entry for air in ", customFootprint," using default"); + airFootprint = 0.1; } const rlm = CustomDatasetHelper.range_limited_motorized; if (!rlm) { - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; + return [ + {low: 0, high: FIVE_KM, optimal: 0}, + {low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir}, + {low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint}]; } else { - console.log('Found range_limited_motorized mode', rlm); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint( - customFootprint, - rlm.kgCo2PerKm, - ); - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm }, - { - low: rlm.range_limit_km * 1000, - high: SIX_HUNDRED_KM, - optimal: lowestMotorizedNonAir, - }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; + console.log("Found range_limited_motorized mode", rlm); + const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(customFootprint, rlm.kgCo2PerKm); + return [ + {low: 0, high: FIVE_KM, optimal: 0}, + {low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm}, + {low: rlm.range_limit_km * 1000, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir}, + {low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint}]; } - } - }; + } + } - return fh; - }) + return fh; +}) - .factory('CalorieCal', function (KVStore, METDatasetHelper, CustomDatasetHelper) { - var cc = {}; - var highestMET = 0; - var USER_DATA_KEY = 'user-data'; - cc.useCustom = false; +.factory('CalorieCal', function(KVStore, METDatasetHelper, CustomDatasetHelper) { - cc.setUseCustomFootprint = function () { - cc.useCustom = true; - }; + var cc = {}; + var highestMET = 0; + var USER_DATA_KEY = "user-data"; + cc.useCustom = false; - cc.getMETs = function () { - if (this.useCustom == true) { + cc.setUseCustomFootprint = function () { + cc.useCustom = true; + } + + cc.getMETs = function() { + if (this.useCustom == true) { return CustomDatasetHelper.getCustomMETs(); - } else { + } else { return METDatasetHelper.getStandardMETs(); - } - }; - - cc.set = function (info) { - return KVStore.set(USER_DATA_KEY, info); - }; - cc.get = function () { - return KVStore.get(USER_DATA_KEY); - }; - cc.delete = function () { - return KVStore.remove(USER_DATA_KEY); - }; - Number.prototype.between = function (min, max) { - return this >= min && this <= max; - }; - cc.getHighestMET = function () { - if (!highestMET) { + } + } + + cc.set = function(info) { + return KVStore.set(USER_DATA_KEY, info); + }; + cc.get = function() { + return KVStore.get(USER_DATA_KEY); + }; + cc.delete = function() { + return KVStore.remove(USER_DATA_KEY); + }; + Number.prototype.between = function (min, max) { + return this >= min && this <= max; + }; + cc.getHighestMET = function() { + if (!highestMET) { var met = cc.getMETs(); let metList = []; for (var mode in met) { - var rangeList = met[mode]; - for (var range in rangeList) { - metList.push(rangeList[range].mets); - } + var rangeList = met[mode]; + for (var range in rangeList) { + metList.push(rangeList[range].mets); + } } highestMET = Math.max(...metList); + } + return highestMET; + } + cc.getMet = function(mode, speed, defaultIfMissing) { + if (mode == 'ON_FOOT') { + console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); + mode = 'WALKING'; + } + let currentMETs = cc.getMETs(); + if (!currentMETs[mode]) { + console.warn("CalorieCal.getMet() Illegal mode: " + mode); + return defaultIfMissing; //So the calorie sum does not break with wrong return type + } + for (var i in currentMETs[mode]) { + if (mpstomph(speed).between(currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { + return currentMETs[mode][i].mets; + } else if (mpstomph(speed) < 0 ) { + console.log("CalorieCal.getMet() Negative speed: " + mpstomph(speed)); + return 0; } - return highestMET; - }; - cc.getMet = function (mode, speed, defaultIfMissing) { - if (mode == 'ON_FOOT') { - console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); - mode = 'WALKING'; - } - let currentMETs = cc.getMETs(); - if (!currentMETs[mode]) { - console.warn('CalorieCal.getMet() Illegal mode: ' + mode); - return defaultIfMissing; //So the calorie sum does not break with wrong return type - } - for (var i in currentMETs[mode]) { - if (mpstomph(speed).between(currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { - return currentMETs[mode][i].mets; - } else if (mpstomph(speed) < 0) { - console.log('CalorieCal.getMet() Negative speed: ' + mpstomph(speed)); - return 0; - } - } - }; - var mpstomph = function (mps) { - return 2.23694 * mps; - }; - var lbtokg = function (lb) { - return lb * 0.453592; - }; - var fttocm = function (ft) { - return ft * 30.48; - }; - cc.getCorrectedMet = function (met, gender, age, height, heightUnit, weight, weightUnit) { - var height = heightUnit == 0 ? fttocm(height) : height; - var weight = weightUnit == 0 ? lbtokg(weight) : weight; - if (gender == 1) { - //male - var met = - (met * 3.5) / - (((66.473 + 5.0033 * height + 13.7516 * weight - 6.755 * age) / 1440 / 5 / weight) * - 1000); - return met; - } else if (gender == 0) { - //female - var met = - (met * 3.5) / - (((655.0955 + 1.8496 * height + 9.5634 * weight - 4.6756 * age) / 1440 / 5 / weight) * - 1000); - return met; - } - }; - cc.getuserCalories = function (durationInMin, met) { - return 65 * durationInMin * met; - }; - cc.getCalories = function (weightInKg, durationInMin, met) { - return weightInKg * durationInMin * met; - }; - return cc; - }); + } + } + var mpstomph = function(mps) { + return 2.23694 * mps; + } + var lbtokg = function(lb) { + return lb * 0.453592; + } + var fttocm = function(ft) { + return ft * 30.48; + } + cc.getCorrectedMet = function(met, gender, age, height, heightUnit, weight, weightUnit) { + var height = heightUnit == 0? fttocm(height) : height; + var weight = weightUnit == 0? lbtokg(weight) : weight; + if (gender == 1) { //male + var met = met*3.5/((66.4730+5.0033*height+13.7516*weight-6.7550*age)/ 1440 / 5 / weight * 1000); + return met; + } else if (gender == 0) { //female + var met = met*3.5/((655.0955+1.8496*height+9.5634*weight-4.6756*age)/ 1440 / 5 / weight * 1000); + return met; + } + } + cc.getuserCalories = function(durationInMin, met) { + return 65 * durationInMin * met; + } + cc.getCalories = function(weightInKg, durationInMin, met) { + return weightInKg * durationInMin * met; + } + return cc; +}); diff --git a/www/js/metrics-mappings.js b/www/js/metrics-mappings.js index ece7a9e6e..e8840bd8c 100644 --- a/www/js/metrics-mappings.js +++ b/www/js/metrics-mappings.js @@ -1,430 +1,403 @@ import angular from 'angular'; import { getLabelOptions } from './survey/multilabel/confirmHelper'; -angular - .module('emission.main.metrics.mappings', [ - 'emission.plugin.logger', - 'emission.plugin.kvstore', - 'emission.config.dynamic', - ]) +angular.module('emission.main.metrics.mappings', ['emission.plugin.logger', + 'emission.plugin.kvstore', + "emission.config.dynamic"]) - .service('CarbonDatasetHelper', function (KVStore) { - var CARBON_DATASET_KEY = 'carbon_dataset_locale'; +.service('CarbonDatasetHelper', function(KVStore) { + var CARBON_DATASET_KEY = 'carbon_dataset_locale'; - // Values are in Kg/PKm (kilograms per passenger-kilometer) - // Sources for EU values: - // - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent - // - HBEFA: 2020, CO2 (per country) - // German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, - // and Tremod for train and air (because HBEFA doesn't provide these). - // EU data is an average of the Tremod/HBEFA data for the countries listed; - // for this average the HBEFA data was used also in the German set (for car and bus). - var carbonDatasets = { - US: { - regionName: 'United States', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 267 / 1609, - BUS: 278 / 1609, - LIGHT_RAIL: 120 / 1609, - SUBWAY: 74 / 1609, - TRAM: 90 / 1609, - TRAIN: 92 / 1609, - AIR_OR_HSR: 217 / 1609, - }, - }, - EU: { - // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) - regionName: 'European Union', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14515, - BUS: 0.04751, - LIGHT_RAIL: 0.064, - SUBWAY: 0.064, - TRAM: 0.064, - TRAIN: 0.048, - AIR_OR_HSR: 0.201, - }, - }, - DE: { - regionName: 'Germany', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.139, // Tremod (passenger car) - BUS: 0.0535, // Tremod (average city/coach) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - FR: { - regionName: 'France', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - AT: { - regionName: 'Austria', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - SE: { - regionName: 'Sweden', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - NO: { - regionName: 'Norway', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - CH: { - regionName: 'Switzerland', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - }; + // Values are in Kg/PKm (kilograms per passenger-kilometer) + // Sources for EU values: + // - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent + // - HBEFA: 2020, CO2 (per country) + // German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, + // and Tremod for train and air (because HBEFA doesn't provide these). + // EU data is an average of the Tremod/HBEFA data for the countries listed; + // for this average the HBEFA data was used also in the German set (for car and bus). + var carbonDatasets = { + US: { + regionName: "United States", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 267/1609, + BUS: 278/1609, + LIGHT_RAIL: 120/1609, + SUBWAY: 74/1609, + TRAM: 90/1609, + TRAIN: 92/1609, + AIR_OR_HSR: 217/1609 + } + }, + EU: { // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) + regionName: "European Union", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.14515, + BUS: 0.04751, + LIGHT_RAIL: 0.064, + SUBWAY: 0.064, + TRAM: 0.064, + TRAIN: 0.048, + AIR_OR_HSR: 0.201 + } + }, + DE: { + regionName: "Germany", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.139, // Tremod (passenger car) + BUS: 0.0535, // Tremod (average city/coach) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201 // Tremod (DE airplane) + } + }, + FR: { + regionName: "France", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201 // Tremod (DE airplane) + } + }, + AT: { + regionName: "Austria", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201 // Tremod (DE airplane) + } + }, + SE: { + regionName: "Sweden", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201 // Tremod (DE airplane) + } + }, + NO: { + regionName: "Norway", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201 // Tremod (DE airplane) + } + }, + CH: { + regionName: "Switzerland", + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201 // Tremod (DE airplane) + } + } + }; - var defaultCarbonDatasetCode = 'US'; - var currentCarbonDatasetCode = defaultCarbonDatasetCode; + var defaultCarbonDatasetCode = 'US'; + var currentCarbonDatasetCode = defaultCarbonDatasetCode; - // we need to call the method from within a promise in initialize() - // and using this.setCurrentCarbonDatasetLocale doesn't seem to work - var setCurrentCarbonDatasetLocale = function (localeCode) { - for (var code in carbonDatasets) { - if (code == localeCode) { - currentCarbonDatasetCode = localeCode; - break; - } + // we need to call the method from within a promise in initialize() + // and using this.setCurrentCarbonDatasetLocale doesn't seem to work + var setCurrentCarbonDatasetLocale = function(localeCode) { + for (var code in carbonDatasets) { + if (code == localeCode) { + currentCarbonDatasetCode = localeCode; + break; } - }; - - this.loadCarbonDatasetLocale = function () { - return KVStore.get(CARBON_DATASET_KEY).then(function (localeCode) { - Logger.log( - 'CarbonDatasetHelper.loadCarbonDatasetLocale() obtained value from storage [' + - localeCode + - ']', - ); - if (!localeCode) { - localeCode = defaultCarbonDatasetCode; - Logger.log( - 'CarbonDatasetHelper.loadCarbonDatasetLocale() no value in storage, using [' + - localeCode + - '] instead', - ); - } - setCurrentCarbonDatasetLocale(localeCode); - }); - }; + } + } - this.saveCurrentCarbonDatasetLocale = function (localeCode) { + this.loadCarbonDatasetLocale = function() { + return KVStore.get(CARBON_DATASET_KEY).then(function(localeCode) { + Logger.log("CarbonDatasetHelper.loadCarbonDatasetLocale() obtained value from storage [" + localeCode + "]"); + if (!localeCode) { + localeCode = defaultCarbonDatasetCode; + Logger.log("CarbonDatasetHelper.loadCarbonDatasetLocale() no value in storage, using [" + localeCode + "] instead"); + } setCurrentCarbonDatasetLocale(localeCode); - KVStore.set(CARBON_DATASET_KEY, currentCarbonDatasetCode); - Logger.log( - 'CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [' + - currentCarbonDatasetCode + - '] to storage', - ); - }; + }); + } - this.getCarbonDatasetOptions = function () { - var options = []; - for (var code in carbonDatasets) { - options.push({ - text: code, //carbonDatasets[code].regionName, - value: code, - }); - } - return options; - }; + this.saveCurrentCarbonDatasetLocale = function (localeCode) { + setCurrentCarbonDatasetLocale(localeCode); + KVStore.set(CARBON_DATASET_KEY, currentCarbonDatasetCode); + Logger.log("CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [" + currentCarbonDatasetCode + "] to storage"); + } - this.getCurrentCarbonDatasetCode = function () { - return currentCarbonDatasetCode; - }; + this.getCarbonDatasetOptions = function() { + var options = []; + for (var code in carbonDatasets) { + options.push({ + text: code, //carbonDatasets[code].regionName, + value: code + }); + } + return options; + }; - this.getCurrentCarbonDatasetFootprint = function () { - return carbonDatasets[currentCarbonDatasetCode].footprintData; - }; - }) - .service('METDatasetHelper', function (KVStore) { - var standardMETs = { - WALKING: { - VERY_SLOW: { - range: [0, 2.0], - mets: 2.0, - }, - SLOW: { - range: [2.0, 2.5], - mets: 2.8, - }, - MODERATE_0: { - range: [2.5, 2.8], - mets: 3.0, - }, - MODERATE_1: { - range: [2.8, 3.2], - mets: 3.5, - }, - FAST: { - range: [3.2, 3.5], - mets: 4.3, - }, - VERY_FAST_0: { - range: [3.5, 4.0], - mets: 5.0, - }, - 'VERY_FAST_!': { - range: [4.0, 4.5], - mets: 6.0, - }, - VERY_VERY_FAST: { - range: [4.5, 5], - mets: 7.0, - }, - SUPER_FAST: { - range: [5, 6], - mets: 8.3, - }, - RUNNING: { - range: [6, Number.MAX_VALUE], - mets: 9.8, - }, + this.getCurrentCarbonDatasetCode = function () { + return currentCarbonDatasetCode; + }; + + this.getCurrentCarbonDatasetFootprint = function () { + return carbonDatasets[currentCarbonDatasetCode].footprintData; + }; +}) +.service('METDatasetHelper', function(KVStore) { + var standardMETs = { + "WALKING": { + "VERY_SLOW": { + range: [0, 2.0], + mets: 2.0 }, - BICYCLING: { - VERY_VERY_SLOW: { - range: [0, 5.5], - mets: 3.5, - }, - VERY_SLOW: { - range: [5.5, 10], - mets: 5.8, - }, - SLOW: { - range: [10, 12], - mets: 6.8, - }, - MODERATE: { - range: [12, 14], - mets: 8.0, - }, - FAST: { - range: [14, 16], - mets: 10.0, - }, - VERT_FAST: { - range: [16, 19], - mets: 12.0, - }, - RACING: { - range: [20, Number.MAX_VALUE], - mets: 15.8, - }, + "SLOW": { + range: [2.0, 2.5], + mets: 2.8 }, - UNKNOWN: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "MODERATE_0": { + range: [2.5, 2.8], + mets: 3.0 }, - IN_VEHICLE: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "MODERATE_1": { + range: [2.8, 3.2], + mets: 3.5 }, - CAR: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "FAST": { + range: [3.2, 3.5], + mets: 4.3 }, - BUS: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "VERY_FAST_0": { + range: [3.5, 4.0], + mets: 5.0 }, - LIGHT_RAIL: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "VERY_FAST_!": { + range: [4.0, 4.5], + mets: 6.0 }, - TRAIN: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "VERY_VERY_FAST": { + range: [4.5, 5], + mets: 7.0 }, - TRAM: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "SUPER_FAST": { + range: [5, 6], + mets: 8.3 }, - SUBWAY: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "RUNNING": { + range: [6, Number.MAX_VALUE], + mets: 9.8 + } + }, + "BICYCLING": { + "VERY_VERY_SLOW": { + range: [0, 5.5], + mets: 3.5 }, - AIR_OR_HSR: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, + "VERY_SLOW": { + range: [5.5, 10], + mets: 5.8 }, - }; - this.getStandardMETs = function () { - return standardMETs; - }; - }) - .factory( - 'CustomDatasetHelper', - function (METDatasetHelper, Logger, $ionicPlatform, DynamicConfig) { - var cdh = {}; + "SLOW": { + range: [10, 12], + mets: 6.8 + }, + "MODERATE": { + range: [12, 14], + mets: 8.0 + }, + "FAST": { + range: [14, 16], + mets: 10.0 + }, + "VERT_FAST": { + range: [16, 19], + mets: 12.0 + }, + "RACING": { + range: [20, Number.MAX_VALUE], + mets: 15.8 + } + }, + "UNKNOWN": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "IN_VEHICLE": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "CAR": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "BUS": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "LIGHT_RAIL": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "TRAIN": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "TRAM": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "SUBWAY": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + }, + "AIR_OR_HSR": { + "ALL": { + range: [0, Number.MAX_VALUE], + mets: 0 + } + } + } + this.getStandardMETs = function() { + return standardMETs; + } +}) +.factory('CustomDatasetHelper', function(METDatasetHelper, Logger, $ionicPlatform, DynamicConfig) { + var cdh = {}; - cdh.getCustomMETs = function () { - console.log('Getting custom METs', cdh.customMETs); + cdh.getCustomMETs = function() { + console.log("Getting custom METs", cdh.customMETs); return cdh.customMETs; - }; + }; - cdh.getCustomFootprint = function () { - console.log('Getting custom footprint', cdh.customPerKmFootprint); + cdh.getCustomFootprint = function() { + console.log("Getting custom footprint", cdh.customPerKmFootprint); return cdh.customPerKmFootprint; - }; + }; - cdh.populateCustomMETs = function () { + cdh.populateCustomMETs = function() { let standardMETs = METDatasetHelper.getStandardMETs(); - let modeOptions = cdh.inputParams['MODE']; + let modeOptions = cdh.inputParams["MODE"]; let modeMETEntries = modeOptions.map((opt) => { - if (opt.met_equivalent) { - let currMET = standardMETs[opt.met_equivalent]; - return [opt.value, currMET]; - } else { - if (opt.met) { - let currMET = opt.met; - // if the user specifies a custom MET, they can't specify - // Number.MAX_VALUE since it is not valid JSON - // we assume that they specify -1 instead, and we will - // map -1 to Number.MAX_VALUE here by iterating over all the ranges - for (const rangeName in currMET) { - // console.log("Handling range ", rangeName); - currMET[rangeName].range = currMET[rangeName].range.map((i) => - i == -1 ? Number.MAX_VALUE : i, - ); - } - return [opt.value, currMET]; + if (opt.met_equivalent) { + let currMET = standardMETs[opt.met_equivalent]; + return [opt.value, currMET]; } else { - console.warn( - 'Did not find either met_equivalent or met for ' + opt.value + ' ignoring entry', - ); - return undefined; + if (opt.met) { + let currMET = opt.met; + // if the user specifies a custom MET, they can't specify + // Number.MAX_VALUE since it is not valid JSON + // we assume that they specify -1 instead, and we will + // map -1 to Number.MAX_VALUE here by iterating over all the ranges + for (const rangeName in currMET) { + // console.log("Handling range ", rangeName); + currMET[rangeName].range = currMET[rangeName].range.map((i) => i == -1? Number.MAX_VALUE : i); + } + return [opt.value, currMET]; + } else { + console.warn("Did not find either met_equivalent or met for " + +opt.value+" ignoring entry"); + return undefined; + } } - } }); cdh.customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); - console.log('After populating, custom METs = ', cdh.customMETs); - }; + console.log("After populating, custom METs = ", cdh.customMETs); + }; - cdh.populateCustomFootprints = function () { - let modeOptions = cdh.inputParams['MODE']; - let modeCO2PerKm = modeOptions - .map((opt) => { + cdh.populateCustomFootprints = function() { + let modeOptions = cdh.inputParams["MODE"]; + let modeCO2PerKm = modeOptions.map((opt) => { if (opt.range_limit_km) { - if (cdh.range_limited_motorized) { - Logger.displayError('Found two range limited motorized options', { - first: cdh.range_limited_motorized, - second: opt, - }); - } - cdh.range_limited_motorized = opt; - console.log('Found range limited motorized mode', cdh.range_limited_motorized); + if (cdh.range_limited_motorized) { + Logger.displayError("Found two range limited motorized options", { + first: cdh.range_limited_motorized, second: opt}); + } + cdh.range_limited_motorized = opt; + console.log("Found range limited motorized mode", cdh.range_limited_motorized); } if (angular.isDefined(opt.kgCo2PerKm)) { - return [opt.value, opt.kgCo2PerKm]; + return [opt.value, opt.kgCo2PerKm]; } else { - return undefined; + return undefined; } - }) - .filter((modeCO2) => angular.isDefined(modeCO2)); + }).filter((modeCO2) => angular.isDefined(modeCO2));; cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); - console.log('After populating, custom perKmFootprint', cdh.customPerKmFootprint); - }; + console.log("After populating, custom perKmFootprint", cdh.customPerKmFootprint); + } - cdh.init = function (newConfig) { - try { - getLabelOptions(newConfig).then((inputParams) => { - console.log('Input params = ', inputParams); - cdh.inputParams = inputParams; - cdh.populateCustomMETs(); - cdh.populateCustomFootprints(); - }); - } catch (e) { - setTimeout(() => { - Logger.displayError( - 'Error in metrics-mappings while initializing custom dataset helper', - e, - ); - }, 1000); - } - }; + cdh.init = function(newConfig) { + try { + getLabelOptions(newConfig).then((inputParams) => { + console.log("Input params = ", inputParams); + cdh.inputParams = inputParams; + cdh.populateCustomMETs(); + cdh.populateCustomFootprints(); + }); + } catch (e) { + setTimeout(() => { + Logger.displayError("Error in metrics-mappings while initializing custom dataset helper", e); + }, 1000); + } + } - $ionicPlatform.ready().then(function () { - DynamicConfig.configReady().then((newConfig) => cdh.init(newConfig)); - }); + $ionicPlatform.ready().then(function() { + DynamicConfig.configReady().then((newConfig) => + cdh.init(newConfig) + ); + }); - return cdh; - }, - ); + return cdh; +}); diff --git a/www/js/metrics/ActiveMinutesTableCard.tsx b/www/js/metrics/ActiveMinutesTableCard.tsx index 2ed26ccfc..ea360ce8e 100644 --- a/www/js/metrics/ActiveMinutesTableCard.tsx +++ b/www/js/metrics/ActiveMinutesTableCard.tsx @@ -2,26 +2,24 @@ import React, { useMemo, useState } from 'react'; import { Card, DataTable, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { - formatDate, - formatDateRangeOfDays, - secondsToMinutes, - segmentDaysByWeeks, -} from './metricsHelper'; +import { formatDate, formatDateRangeOfDays, secondsToMinutes, segmentDaysByWeeks } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import { ACTIVE_MODES } from './WeeklyActiveMinutesCard'; import { labelKeyToRichMode } from '../survey/multilabel/confirmHelper'; -type Props = { userMetrics: MetricsData }; +type Props = { userMetrics: MetricsData } const ActiveMinutesTableCard = ({ userMetrics }: Props) => { + const { colors } = useTheme(); const { t } = useTranslation(); const cumulativeTotals = useMemo(() => { if (!userMetrics?.duration) return []; const totals = {}; - ACTIVE_MODES.forEach((mode) => { - const sum = userMetrics.duration.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); + ACTIVE_MODES.forEach(mode => { + const sum = userMetrics.duration.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); totals[mode] = secondsToMinutes(sum); }); totals['period'] = formatDateRangeOfDays(userMetrics.duration); @@ -30,32 +28,30 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => { const recentWeeksActiveModesTotals = useMemo(() => { if (!userMetrics?.duration) return []; - return segmentDaysByWeeks(userMetrics.duration) - .reverse() - .map((week) => { - const totals = {}; - ACTIVE_MODES.forEach((mode) => { - const sum = week.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); - totals[mode] = secondsToMinutes(sum); - }); - totals['period'] = formatDateRangeOfDays(week); - return totals; - }); + return segmentDaysByWeeks(userMetrics.duration).reverse().map(week => { + const totals = {}; + ACTIVE_MODES.forEach(mode => { + const sum = week.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); + totals[mode] = secondsToMinutes(sum); + }) + totals['period'] = formatDateRangeOfDays(week); + return totals; + }); }, [userMetrics?.duration]); const dailyActiveModesTotals = useMemo(() => { if (!userMetrics?.duration) return []; - return userMetrics.duration - .map((day) => { - const totals = {}; - ACTIVE_MODES.forEach((mode) => { - const sum = day[`label_${mode}`] || 0; - totals[mode] = secondsToMinutes(sum); - }); - totals['period'] = formatDate(day); - return totals; + return userMetrics.duration.map(day => { + const totals = {}; + ACTIVE_MODES.forEach(mode => { + const sum = day[`label_${mode}`] || 0; + totals[mode] = secondsToMinutes(sum); }) - .reverse(); + totals['period'] = formatDate(day); + return totals; + }).reverse(); }, [userMetrics?.duration]); const allTotals = [cumulativeTotals, ...recentWeeksActiveModesTotals, ...dailyActiveModesTotals]; @@ -66,46 +62,38 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => { const to = Math.min((page + 1) * itemsPerPage, allTotals.length); return ( - + + style={cardStyles.title(colors)} /> - {ACTIVE_MODES.map((mode, i) => ( - - {labelKeyToRichMode(mode)} - - ))} + {ACTIVE_MODES.map((mode, i) => + {labelKeyToRichMode(mode)} + )} - {allTotals.slice(from, to).map((total, i) => ( - + {allTotals.slice(from, to).map((total, i) => + {total['period']} - {ACTIVE_MODES.map((mode, j) => ( - - {total[mode]} {t('metrics.minutes')} - - ))} + {ACTIVE_MODES.map((mode, j) => + {total[mode]} {t('metrics.minutes')} + )} - ))} - setPage(p)} - numberOfPages={Math.ceil(allTotals.length / 5)} - numberOfItemsPerPage={5} - label={`${page * 5 + 1}-${page * 5 + 5} of ${allTotals.length}`} - /> + )} + setPage(p)} + numberOfPages={Math.ceil(allTotals.length / 5)} numberOfItemsPerPage={5} + label={`${page * 5 + 1}-${page * 5 + 5} of ${allTotals.length}`} /> - ); -}; + ) +} export default ActiveMinutesTableCard; diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 7c9bf3891..6012cb61a 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -1,240 +1,168 @@ import React, { useState, useMemo } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme } from 'react-native-paper'; +import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { - formatDateRangeOfDays, - parseDataFromMetrics, - generateSummaryFromData, - calculatePercentChange, - segmentDaysByWeeks, - isCustomLabels, -} from './metricsHelper'; +import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks, isCustomLabels } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; -import color from 'color'; +import color from "color"; -type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; +type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { - const FootprintHelper = getAngularService('FootprintHelper'); - const { colors } = useTheme(); - const { t } = useTranslation(); - - const [emissionsChange, setEmissionsChange] = useState({}); - - const userCarbonRecords = useMemo(() => { - if (userMetrics?.distance?.length > 0) { - //separate data into weeks - const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); - - //formatted data from last week, if exists (14 days ago -> 8 days ago) - let userLastWeekModeMap = {}; - let userLastWeekSummaryMap = {}; - if (lastWeekDistance && lastWeekDistance?.length == 7) { - userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); - userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); - } - - //formatted distance data from this week (7 days ago -> yesterday) - let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); - let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); - let worstDistance = userThisWeekSummaryMap.reduce( - (prevDistance, currModeSummary) => prevDistance + currModeSummary.values, - 0, - ); - - //setting up data to be displayed - let graphRecords = []; - - //set custon dataset, if the labels are custom - if (isCustomLabels(userThisWeekModeMap)) { - FootprintHelper.setUseCustomFootprint(); - } - - //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) - let userPrevWeek; - if (userLastWeekSummaryMap[0]) { - userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userLastWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), - }; - graphRecords.push({ - label: t('main-metrics.unlabeled'), - x: userPrevWeek.high - userPrevWeek.low, - y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, - }); - graphRecords.push({ - label: t('main-metrics.labeled'), - x: userPrevWeek.low, - y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, - }); - } - - //calculate low-high and format range for past week (7 days ago -> yesterday) - let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userThisWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), - }; - graphRecords.push({ - label: t('main-metrics.unlabeled'), - x: userPastWeek.high - userPastWeek.low, - y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, - }); - graphRecords.push({ - label: t('main-metrics.labeled'), - x: userPastWeek.low, - y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, - }); - if (userPrevWeek) { - let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); - setEmissionsChange(pctChange); - } - - //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - graphRecords.push({ - label: t('main-metrics.labeled'), - x: worstCarbon, - y: `${t('main-metrics.worst-case')}`, - }); - - return graphRecords; - } - }, [userMetrics?.distance]); - - const groupCarbonRecords = useMemo(() => { - if (aggMetrics?.distance?.length > 0) { - //separate data into weeks - const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; - console.log('testing agg metrics', aggMetrics, thisWeekDistance); - - let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'aggregate'); - let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, 'distance'); - - // Issue 422: - // https://github.com/e-mission/e-mission-docs/issues/422 - let aggCarbonData = []; - for (var i in aggThisWeekSummary) { - aggCarbonData.push(aggThisWeekSummary[i]); - if (isNaN(aggCarbonData[i].values)) { - console.warn( - 'WARNING in calculating groupCarbonRecords: value is NaN for mode ' + - aggCarbonData[i].key + - ', changing to 0', - ); - aggCarbonData[i].values = 0; + const FootprintHelper = getAngularService("FootprintHelper"); + const { colors } = useTheme(); + const { t } = useTranslation(); + + const [emissionsChange, setEmissionsChange] = useState({}); + + const userCarbonRecords = useMemo(() => { + if(userMetrics?.distance?.length > 0) { + //separate data into weeks + const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); + + //formatted data from last week, if exists (14 days ago -> 8 days ago) + let userLastWeekModeMap = {}; + let userLastWeekSummaryMap = {}; + if(lastWeekDistance && lastWeekDistance?.length == 7) { + userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); + userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); + } + + //formatted distance data from this week (7 days ago -> yesterday) + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + + //setting up data to be displayed + let graphRecords = []; + + //set custon dataset, if the labels are custom + if(isCustomLabels(userThisWeekModeMap)){ + FootprintHelper.setUseCustomFootprint(); + } + + //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) + let userPrevWeek; + if(userLastWeekSummaryMap[0]) { + userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) + }; + graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) + graphRecords.push({label: t('main-metrics.labeled'), x: userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); + } + + //calculate low-high and format range for past week (7 days ago -> yesterday) + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), + }; + graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) + graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + if (userPrevWeek) { + let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); + setEmissionsChange(pctChange); + } + + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + graphRecords.push({label: t('main-metrics.labeled'), x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); + + return graphRecords; } - } - - let groupRecords = []; - - let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics( - aggCarbonData, - FootprintHelper.getHighestFootprint(), - ), - }; - console.log('testing group past week', aggCarbon); - groupRecords.push({ - label: t('main-metrics.unlabeled'), - x: aggCarbon.high - aggCarbon.low, - y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, - }); - groupRecords.push({ - label: t('main-metrics.labeled'), - x: aggCarbon.low, - y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, - }); - - return groupRecords; - } - }, [aggMetrics]); - - const chartData = useMemo(() => { - let tempChartData = []; - if (userCarbonRecords?.length) { - tempChartData = tempChartData.concat(userCarbonRecords); - } - if (groupCarbonRecords?.length) { - tempChartData = tempChartData.concat(groupCarbonRecords); - } - tempChartData = tempChartData.reverse(); - console.log('testing chart data', tempChartData); - return tempChartData; - }, [userCarbonRecords, groupCarbonRecords]); - - const cardSubtitleText = useMemo(() => { - const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2) - .reverse() - .flat(); - const recentEntriesRange = formatDateRangeOfDays(recentEntries); - return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; - }, [aggMetrics?.distance]); - - //hardcoded here, could be read from config at later customization? - let carbonGoals = [ - { - label: t('main-metrics.us-2050-goal'), - value: 14, - color: color(colors.warn).darken(0.65).saturate(0.5).rgb().toString(), - }, - { - label: t('main-metrics.us-2030-goal'), - value: 54, - color: color(colors.danger).saturate(0.5).rgb().toString(), - }, - ]; - let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; - - return ( - - } - style={cardStyles.title(colors)} - /> - - {chartData?.length > 0 ? ( - - - - {t('main-metrics.us-goals-footnote')} - - - ) : ( - - - {t('metrics.chart-no-data')} - - - )} - - - ); -}; + }, [userMetrics?.distance]) + + const groupCarbonRecords = useMemo(() => { + if(aggMetrics?.distance?.length > 0) + { + //separate data into weeks + const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; + console.log("testing agg metrics" , aggMetrics, thisWeekDistance); + + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); + + // Issue 422: + // https://github.com/e-mission/e-mission-docs/issues/422 + let aggCarbonData = []; + for (var i in aggThisWeekSummary) { + aggCarbonData.push(aggThisWeekSummary[i]); + if (isNaN(aggCarbonData[i].values)) { + console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); + aggCarbonData[i].values = 0; + } + } + + let groupRecords = []; + + let aggCarbon = { + low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), + high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), + } + console.log("testing group past week", aggCarbon); + groupRecords.push({label: t('main-metrics.unlabeled'), x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + groupRecords.push({label: t('main-metrics.labeled'), x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); + + return groupRecords; + } + }, [aggMetrics]) + + const chartData = useMemo(() => { + let tempChartData = []; + if(userCarbonRecords?.length) { + tempChartData = tempChartData.concat(userCarbonRecords); + } + if(groupCarbonRecords?.length) { + tempChartData = tempChartData.concat(groupCarbonRecords); + } + tempChartData = tempChartData.reverse(); + console.log("testing chart data", tempChartData); + return tempChartData; + }, [userCarbonRecords, groupCarbonRecords]); + + const cardSubtitleText = useMemo(() => { + const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2).reverse().flat(); + const recentEntriesRange = formatDateRangeOfDays(recentEntries); + return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; + }, [aggMetrics?.distance]); + + //hardcoded here, could be read from config at later customization? + let carbonGoals = [ {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.65).saturate(.5).rgb().toString()}, + {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).saturate(.5).rgb().toString()} ]; + let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; + + return ( + + } + style={cardStyles.title(colors)} /> + + { chartData?.length > 0 ? + + + + {t('main-metrics.us-goals-footnote')} + + + : + + + {t('metrics.chart-no-data')} + + } + + + ) +} export default CarbonFootprintCard; diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 9f1b4490f..223ae709f 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -1,189 +1,151 @@ import React, { useMemo } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme } from 'react-native-paper'; +import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; -import { - formatDateRangeOfDays, - parseDataFromMetrics, - generateSummaryFromData, - calculatePercentChange, - segmentDaysByWeeks, -} from './metricsHelper'; +import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks } from './metricsHelper'; import { getAngularService } from '../angular-react-helper'; -type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; +type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { + const { colors } = useTheme(); const { t } = useTranslation(); - const FootprintHelper = getAngularService('FootprintHelper'); + const FootprintHelper = getAngularService("FootprintHelper"); const userText = useMemo(() => { - if (userMetrics?.distance?.length > 0) { - //separate data into weeks - const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); - - //formatted data from last week, if exists (14 days ago -> 8 days ago) - let userLastWeekModeMap = {}; - let userLastWeekSummaryMap = {}; - if (lastWeekDistance && lastWeekDistance?.length == 7) { - userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); - userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); - } - - //formatted distance data from this week (7 days ago -> yesterday) - let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); - let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); - let worstDistance = userThisWeekSummaryMap.reduce( - (prevDistance, currModeSummary) => prevDistance + currModeSummary.values, - 0, - ); - - //setting up data to be displayed - let textList = []; + if(userMetrics?.distance?.length > 0) { + //separate data into weeks + const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); + + //formatted data from last week, if exists (14 days ago -> 8 days ago) + let userLastWeekModeMap = {}; + let userLastWeekSummaryMap = {}; + if(lastWeekDistance && lastWeekDistance?.length == 7) { + userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); + userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); + } + + //formatted distance data from this week (7 days ago -> yesterday) + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); + + //setting up data to be displayed + let textList = []; + + //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) + if(userLastWeekSummaryMap[0]) { + let userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) + }; + const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; + if (userPrevWeek.low == userPrevWeek.high) + textList.push({label: label, value: Math.round(userPrevWeek.low)}); + else + textList.push({label: label + '²', value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`}); + } - //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) - if (userLastWeekSummaryMap[0]) { - let userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userLastWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + //calculate low-high and format range for past week (7 days ago -> yesterday) + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), }; - const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; - if (userPrevWeek.low == userPrevWeek.high) - textList.push({ label: label, value: Math.round(userPrevWeek.low) }); + const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; + if (userPastWeek.low == userPastWeek.high) + textList.push({label: label, value: Math.round(userPastWeek.low)}); else - textList.push({ - label: label + '²', - value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`, - }); - } - - //calculate low-high and format range for past week (7 days ago -> yesterday) - let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userThisWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), - }; - const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; - if (userPastWeek.low == userPastWeek.high) - textList.push({ label: label, value: Math.round(userPastWeek.low) }); - else - textList.push({ - label: label + '²', - value: `${Math.round(userPastWeek.low)} - ${Math.round(userPastWeek.high)}`, - }); + textList.push({label: label + '²', value: `${Math.round(userPastWeek.low)} - ${Math.round(userPastWeek.high)}`}); + + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + textList.push({label:t('main-metrics.worst-case'), value: Math.round(worstCarbon)}); - //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - textList.push({ label: t('main-metrics.worst-case'), value: Math.round(worstCarbon) }); - - return textList; + return textList; } }, [userMetrics]); const groupText = useMemo(() => { - if (aggMetrics?.distance?.length > 0) { - //separate data into weeks - const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; - - let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'aggregate'); - let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, 'distance'); - - // Issue 422: - // https://github.com/e-mission/e-mission-docs/issues/422 - let aggCarbonData = []; - for (var i in aggThisWeekSummary) { - aggCarbonData.push(aggThisWeekSummary[i]); - if (isNaN(aggCarbonData[i].values)) { - console.warn( - 'WARNING in calculating groupCarbonRecords: value is NaN for mode ' + - aggCarbonData[i].key + - ', changing to 0', - ); - aggCarbonData[i].values = 0; + if(aggMetrics?.distance?.length > 0) + { + //separate data into weeks + const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; + + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); + + // Issue 422: + // https://github.com/e-mission/e-mission-docs/issues/422 + let aggCarbonData = []; + for (var i in aggThisWeekSummary) { + aggCarbonData.push(aggThisWeekSummary[i]); + if (isNaN(aggCarbonData[i].values)) { + console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); + aggCarbonData[i].values = 0; + } } - } - let groupText = []; + let groupText = []; - let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics( - aggCarbonData, - FootprintHelper.getHighestFootprint(), - ), - }; - console.log('testing group past week', aggCarbon); - const label = t('main-metrics.average'); - if (aggCarbon.low == aggCarbon.high) - groupText.push({ label: label, value: Math.round(aggCarbon.low) }); - else - groupText.push({ - label: label + '²', - value: `${Math.round(aggCarbon.low)} - ${Math.round(aggCarbon.high)}`, - }); + let aggCarbon = { + low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), + high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), + } + console.log("testing group past week", aggCarbon); + const label = t('main-metrics.average'); + if (aggCarbon.low == aggCarbon.high) + groupText.push({label: label, value: Math.round(aggCarbon.low)}); + else + groupText.push({label: label + '²', value: `${Math.round(aggCarbon.low)} - ${Math.round(aggCarbon.high)}`}); - return groupText; + return groupText; } }, [aggMetrics]); const textEntries = useMemo(() => { - let tempText = []; - if (userText?.length) { - tempText = tempText.concat(userText); + let tempText = [] + if(userText?.length){ + tempText = tempText.concat(userText); } - if (groupText?.length) { - tempText = tempText.concat(groupText); + if(groupText?.length) { + tempText = tempText.concat(groupText); } return tempText; }, [userText, groupText]); - + const cardSubtitleText = useMemo(() => { - const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2) - .reverse() - .flat(); + const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2).reverse().flat(); const recentEntriesRange = formatDateRangeOfDays(recentEntries); return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; }, [aggMetrics?.distance]); return ( - - + - - {textEntries?.length > 0 && - Object.keys(textEntries).map((i) => ( - - {textEntries[i].label} - {textEntries[i].value + ' ' + 'kg CO₂'} + style={cardStyles.title(colors)} /> + + { textEntries?.length > 0 && + Object.keys(textEntries).map((i) => + + {textEntries[i].label} + {textEntries[i].value + ' ' + "kg CO₂"} - ))} - - {t('main-metrics.range-uncertain-footnote')} + ) + } + + {t('main-metrics.range-uncertain-footnote')} - + - ); -}; + ) +} export default CarbonTextCard; diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index a2373faf3..eafd3460e 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -1,72 +1,79 @@ -import React, { useMemo } from 'react'; +import React, {useMemo} from 'react'; import { View } from 'react-native'; -import { useTheme, Text } from 'react-native-paper'; +import { useTheme, Text } from "react-native-paper"; import { useTranslation } from 'react-i18next'; -import colorLib from 'color'; +import colorLib from "color"; type Props = { - change: { low: number; high: number }; -}; + change: {low: number, high: number}, +} const ChangeIndicator = ({ change }) => { - const { colors } = useTheme(); - const { t } = useTranslation(); + const { colors } = useTheme(); + const { t } = useTranslation(); - const changeSign = function (changeNum) { - if (changeNum > 0) { - return '+'; - } else { - return '-'; - } - }; + const changeSign = function(changeNum) { + if(changeNum > 0) { + return "+"; + } else { + return "-"; + } + }; - const changeText = useMemo(() => { - if (change) { - let low = isFinite(change.low) ? Math.round(Math.abs(change.low)) : '∞'; - let high = isFinite(change.high) ? Math.round(Math.abs(change.high)) : '∞'; - - if (Math.round(change.low) == Math.round(change.high)) { - let text = changeSign(change.low) + low + '%'; - return text; - } else if (!(isFinite(change.low) || isFinite(change.high))) { - return ''; //if both are not finite, no information is really conveyed, so don't show - } else { - let text = `${changeSign(change.low) + low}% / ${changeSign(change.high) + high}%`; - return text; - } - } - }, [change]); - - return changeText != '' ? ( - 0 ? colors.danger : colors.success)}> - {changeText + '\n'} - {`${t('metrics.this-week')}`} - - ) : ( - <> - ); -}; + const changeText = useMemo(() => { + if(change) { + let low = isFinite(change.low) ? Math.round(Math.abs(change.low)): '∞'; + let high = isFinite(change.high) ? Math.round(Math.abs(change.high)) : '∞'; + + if(Math.round(change.low) == Math.round(change.high)) + { + let text = changeSign(change.low) + low + "%"; + return text; + } else if(!(isFinite(change.low) || isFinite(change.high))) { + return ""; //if both are not finite, no information is really conveyed, so don't show + } + else { + let text = `${changeSign(change.low) + low}% / ${changeSign(change.high) + high}%`; + return text; + } + } + },[change]) + + return ( + (changeText != "") ? + 0 ? colors.danger : colors.success)}> + + {changeText + '\n'} + + + {`${t("metrics.this-week")}`} + + + : + <> + ) +} const styles: any = { - text: (colors) => ({ - color: colors.onPrimary, - fontWeight: '400', - textAlign: 'center', - }), - importantText: (colors) => ({ - color: colors.onPrimary, - fontWeight: '500', - textAlign: 'center', - fontSize: 16, - }), - view: (color) => ({ - backgroundColor: colorLib(color).alpha(0.85).rgb().toString(), - padding: 2, - borderStyle: 'solid', - borderColor: colorLib(color).darken(0.4).rgb().toString(), - borderWidth: 2.5, - borderRadius: 10, - }), -}; - + text: (colors) => ({ + color: colors.onPrimary, + fontWeight: '400', + textAlign: 'center' + }), + importantText: (colors) => ({ + color: colors.onPrimary, + fontWeight: '500', + textAlign: 'center', + fontSize: 16, + }), + view: (color) => ({ + backgroundColor: colorLib(color).alpha(0.85).rgb().toString(), + padding: 2, + borderStyle: 'solid', + borderColor: colorLib(color).darken(0.4).rgb().toString(), + borderWidth: 2.5, + borderRadius: 10, + }), +} + export default ChangeIndicator; diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx index acaf9c1ed..479a5f5b5 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -1,6 +1,7 @@ + import React, { useMemo } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme } from 'react-native-paper'; +import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; @@ -9,18 +10,19 @@ import LineChart from '../components/LineChart'; import { getBaseModeByText } from '../diary/diaryHelper'; const ACTIVE_MODES = ['walk', 'bike'] as const; -type ActiveMode = (typeof ACTIVE_MODES)[number]; +type ActiveMode = typeof ACTIVE_MODES[number]; -type Props = { userMetrics: MetricsData }; +type Props = { userMetrics: MetricsData } const DailyActiveMinutesCard = ({ userMetrics }: Props) => { + const { colors } = useTheme(); const { t } = useTranslation(); const dailyActiveMinutesRecords = useMemo(() => { const records = []; const recentDays = userMetrics?.duration?.slice(-14); - recentDays?.forEach((day) => { - ACTIVE_MODES.forEach((mode) => { + recentDays?.forEach(day => { + ACTIVE_MODES.forEach(mode => { const activeSeconds = day[`label_${mode}`]; records.push({ label: labelKeyToRichMode(mode), @@ -29,38 +31,34 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { }); }); }); - return records as { label: ActiveMode; x: string; y: number }[]; + return records as {label: ActiveMode, x: string, y: number}[]; }, [userMetrics?.duration]); return ( - - + + style={cardStyles.title(colors)} /> - {dailyActiveMinutesRecords.length ? ( - getBaseModeByText(l, labelOptions).color} - /> - ) : ( - - + { dailyActiveMinutesRecords.length ? + getBaseModeByText(l, labelOptions).color} /> + : + + {t('metrics.chart-no-data')} - )} + } ); -}; +} export default DailyActiveMinutesCard; diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 1727d6e49..7a0f8c8bc 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,7 +1,8 @@ + import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; import { Card, Checkbox, Text, useTheme } from 'react-native-paper'; -import colorLib from 'color'; +import colorLib from "color"; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { formatDateRangeOfDays, getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; @@ -12,36 +13,30 @@ import { getBaseModeByKey, getBaseModeByText } from '../diary/diaryHelper'; import { useTranslation } from 'react-i18next'; type Props = { - cardTitle: string; - userMetricsDays: DayOfMetricData[]; - aggMetricsDays: DayOfMetricData[]; - axisUnits: string; - unitFormatFn?: (val: number) => string | number; -}; -const MetricsCard = ({ - cardTitle, - userMetricsDays, - aggMetricsDays, - axisUnits, - unitFormatFn, -}: Props) => { - const { colors } = useTheme(); + cardTitle: string, + userMetricsDays: DayOfMetricData[], + aggMetricsDays: DayOfMetricData[], + axisUnits: string, + unitFormatFn?: (val: number) => string|number, +} +const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, unitFormatFn}: Props) => { + + const { colors } = useTheme(); const { t } = useTranslation(); - const [viewMode, setViewMode] = useState<'details' | 'graph'>('details'); - const [populationMode, setPopulationMode] = useState<'user' | 'aggregate'>('user'); + const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); + const [populationMode, setPopulationMode] = useState<'user'|'aggregate'>('user'); const [graphIsStacked, setGraphIsStacked] = useState(true); - const metricDataDays = useMemo( - () => (populationMode == 'user' ? userMetricsDays : aggMetricsDays), - [populationMode, userMetricsDays, aggMetricsDays], - ); + const metricDataDays = useMemo(() => ( + populationMode == 'user' ? userMetricsDays : aggMetricsDays + ), [populationMode, userMetricsDays, aggMetricsDays]); // for each label on each day, create a record for the chart const chartData = useMemo(() => { if (!metricDataDays || viewMode != 'graph') return []; - const records: { label: string; x: string | number; y: string | number }[] = []; - metricDataDays.forEach((day) => { + const records: {label: string, x: string|number, y: string|number}[] = []; + metricDataDays.forEach(day => { const labels = getLabelsForDay(day); - labels.forEach((label) => { + labels.forEach(label => { const rawVal = day[`label_${label}`]; records.push({ label: labelKeyToRichMode(label), @@ -52,7 +47,7 @@ const MetricsCard = ({ }); // sort records (affects the order they appear in the chart legend) records.sort((a, b) => { - if (a.label == 'Unlabeled') return 1; // sort Unlabeled to the end + if (a.label == 'Unlabeled') return 1; // sort Unlabeled to the end if (b.label == 'Unlabeled') return -1; // sort Unlabeled to the end return (a.y as number) - (b.y as number); // otherwise, just sort by time }); @@ -60,8 +55,8 @@ const MetricsCard = ({ }, [metricDataDays, viewMode]); const cardSubtitleText = useMemo(() => { - const groupText = - populationMode == 'user' ? t('main-metrics.user-totals') : t('main-metrics.group-totals'); + const groupText = populationMode == 'user' ? t('main-metrics.user-totals') + : t('main-metrics.group-totals'); return `${groupText} (${formatDateRangeOfDays(metricDataDays)})`; }, [metricDataDays, populationMode]); @@ -72,8 +67,10 @@ const MetricsCard = ({ // for each label, sum up cumulative values across all days const vals = {}; - uniqueLabels.forEach((label) => { - const sum = metricDataDays.reduce((acc, day) => acc + (day[`label_${label}`] || 0), 0); + uniqueLabels.forEach(label => { + const sum = metricDataDays.reduce((acc, day) => ( + acc + (day[`label_${label}`] || 0) + ), 0); vals[label] = unitFormatFn ? unitFormatFn(sum) : sum; }); return vals; @@ -82,84 +79,55 @@ const MetricsCard = ({ // Unlabelled data shows up as 'UNKNOWN' grey and mostly transparent // All other modes are colored according to their base mode const getColorForLabel = (label: string) => { - if (label == 'Unlabeled') { + if (label == "Unlabeled") { const unknownModeColor = getBaseModeByKey('UNKNOWN').color; return colorLib(unknownModeColor).alpha(0.15).rgb().string(); } return getBaseModeByText(label, labelOptions).color; - }; + } return ( - ( - - setViewMode(v as any)} - buttons={[ - { icon: 'abacus', value: 'details' }, - { icon: 'chart-bar', value: 'graph' }, - ]} - /> - setPopulationMode(p as any)} - buttons={[ - { icon: 'account', value: 'user' }, - { icon: 'account-group', value: 'aggregate' }, - ]} - /> + right={() => + + setViewMode(v as any)} + buttons={[{ icon: 'abacus', value: 'details' }, { icon: 'chart-bar', value: 'graph' }]} /> + setPopulationMode(p as any)} + buttons={[{ icon: 'account', value: 'user' }, { icon: 'account-group', value: 'aggregate' }]} /> - )} - style={cardStyles.title(colors)} - /> + } + style={cardStyles.title(colors)} /> - {viewMode == 'details' && ( - - {Object.keys(metricSumValues).map((label, i) => ( + {viewMode=='details' && + + { Object.keys(metricSumValues).map((label, i) => - {labelKeyToRichMode(label)} + {labelKeyToRichMode(label)} {metricSumValues[label] + ' ' + axisUnits} - ))} + )} + + } + {viewMode=='graph' && <> + + + Stack bars: + setGraphIsStacked(!graphIsStacked)} /> - )} - {viewMode == 'graph' && ( - <> - - - Stack bars: - setGraphIsStacked(!graphIsStacked)} - /> - - - )} + } - ); -}; + ) +} export default MetricsCard; diff --git a/www/js/metrics/MetricsDateSelect.tsx b/www/js/metrics/MetricsDateSelect.tsx index fa1aaed3e..c66218453 100644 --- a/www/js/metrics/MetricsDateSelect.tsx +++ b/www/js/metrics/MetricsDateSelect.tsx @@ -6,78 +6,66 @@ and allows the user to select a date. */ -import React, { useState, useCallback, useMemo } from 'react'; -import { Text, StyleSheet } from 'react-native'; -import { DatePickerModal } from 'react-native-paper-dates'; -import { Divider, useTheme } from 'react-native-paper'; -import i18next from 'i18next'; -import { useTranslation } from 'react-i18next'; -import NavBarButton from '../components/NavBarButton'; -import { DateTime } from 'luxon'; +import React, { useState, useCallback, useMemo } from "react"; +import { Text, StyleSheet } from "react-native"; +import { DatePickerModal } from "react-native-paper-dates"; +import { Divider, useTheme } from "react-native-paper"; +import i18next from "i18next"; +import { useTranslation } from "react-i18next"; +import NavBarButton from "../components/NavBarButton"; +import { DateTime } from "luxon"; type Props = { - dateRange: DateTime[]; - setDateRange: (dateRange: [DateTime, DateTime]) => void; -}; + dateRange: DateTime[], + setDateRange: (dateRange: [DateTime, DateTime]) => void, +} const MetricsDateSelect = ({ dateRange, setDateRange }: Props) => { + const { t } = useTranslation(); const { colors } = useTheme(); const [open, setOpen] = useState(false); const todayDate = useMemo(() => new Date(), []); - const dateRangeAsJSDate = useMemo( - () => [dateRange[0].toJSDate(), dateRange[1].toJSDate()], - [dateRange], - ); + const dateRangeAsJSDate = useMemo(() => + [ dateRange[0].toJSDate(), dateRange[1].toJSDate() ], + [dateRange]); const onDismiss = useCallback(() => { setOpen(false); }, [setOpen]); - const onChoose = useCallback( - ({ startDate, endDate }) => { - setOpen(false); - setDateRange([ - DateTime.fromJSDate(startDate).startOf('day'), - DateTime.fromJSDate(endDate).startOf('day'), - ]); - }, - [setOpen, setDateRange], - ); + const onChoose = useCallback(({ startDate, endDate }) => { + setOpen(false); + setDateRange([ + DateTime.fromJSDate(startDate).startOf('day'), + DateTime.fromJSDate(endDate).startOf('day') + ]); + }, [setOpen, setDateRange]); - return ( - <> - setOpen(true)}> - {dateRange[0] && ( - <> - {dateRange[0].toLocaleString()} - - - )} - {dateRange[1]?.toLocaleString() || t('diary.today')} - - - - ); + return (<> + setOpen(true)}> + {dateRange[0] && (<> + {dateRange[0].toLocaleString()} + + )} + {dateRange[1]?.toLocaleString() || t('diary.today')} + + + ); }; export const s = StyleSheet.create({ divider: { width: '3ch', marginHorizontal: 'auto', - }, + } }); export default MetricsDateSelect; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 82e85082d..25020b435 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -1,35 +1,36 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { angularize, getAngularService } from '../angular-react-helper'; -import { View, ScrollView, useWindowDimensions } from 'react-native'; -import { Appbar } from 'react-native-paper'; -import NavBarButton from '../components/NavBarButton'; -import { useTranslation } from 'react-i18next'; -import { DateTime } from 'luxon'; -import { MetricsData } from './metricsTypes'; -import MetricsCard from './MetricsCard'; -import { formatForDisplay, useImperialConfig } from '../config/useImperialConfig'; -import MetricsDateSelect from './MetricsDateSelect'; -import WeeklyActiveMinutesCard from './WeeklyActiveMinutesCard'; -import { secondsToHours, secondsToMinutes } from './metricsHelper'; -import CarbonFootprintCard from './CarbonFootprintCard'; -import Carousel from '../components/Carousel'; -import DailyActiveMinutesCard from './DailyActiveMinutesCard'; -import CarbonTextCard from './CarbonTextCard'; -import ActiveMinutesTableCard from './ActiveMinutesTableCard'; +import React, { useEffect, useState, useMemo } from "react"; +import { angularize, getAngularService } from "../angular-react-helper"; +import { View, ScrollView, useWindowDimensions } from "react-native"; +import { Appbar } from "react-native-paper"; +import NavBarButton from "../components/NavBarButton"; +import { useTranslation } from "react-i18next"; +import { DateTime } from "luxon"; +import { MetricsData } from "./metricsTypes"; +import MetricsCard from "./MetricsCard"; +import { formatForDisplay, useImperialConfig } from "../config/useImperialConfig"; +import MetricsDateSelect from "./MetricsDateSelect"; +import WeeklyActiveMinutesCard from "./WeeklyActiveMinutesCard"; +import { secondsToHours, secondsToMinutes } from "./metricsHelper"; +import CarbonFootprintCard from "./CarbonFootprintCard"; +import Carousel from "../components/Carousel"; +import DailyActiveMinutesCard from "./DailyActiveMinutesCard"; +import CarbonTextCard from "./CarbonTextCard"; +import ActiveMinutesTableCard from "./ActiveMinutesTableCard"; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; -async function fetchMetricsFromServer(type: 'user' | 'aggregate', dateRange: DateTime[]) { +async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: DateTime[]) { const CommHelper = getAngularService('CommHelper'); const query = { freq: 'D', start_time: dateRange[0].toSeconds(), end_time: dateRange[1].toSeconds(), metric_list: METRIC_LIST, - is_return_aggregate: type == 'aggregate', - }; - if (type == 'user') return CommHelper.getMetrics('timestamp', query); - return CommHelper.getAggregateData('result/metrics/timestamp', query); + is_return_aggregate: (type == 'aggregate'), + } + if (type == 'user') + return CommHelper.getMetrics('timestamp', query); + return CommHelper.getAggregateData("result/metrics/timestamp", query); } function getLastTwoWeeksDtRange() { @@ -40,9 +41,10 @@ function getLastTwoWeeksDtRange() { } const MetricsTab = () => { + const { t } = useTranslation(); - const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = - useImperialConfig(); + const { getFormattedSpeed, speedSuffix, + getFormattedDistance, distanceSuffix } = useImperialConfig(); const [dateRange, setDateRange] = useState(getLastTwoWeeksDtRange); const [aggMetrics, setAggMetrics] = useState(null); @@ -53,11 +55,11 @@ const MetricsTab = () => { loadMetricsForPopulation('aggregate', dateRange); }, [dateRange]); - async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { + async function loadMetricsForPopulation(population: 'user'|'aggregate', dateRange: DateTime[]) { const serverResponse = await fetchMetricsFromServer(population, dateRange); - console.debug('Got metrics = ', serverResponse); + console.debug("Got metrics = ", serverResponse); const metrics = {}; - const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; + const dataKey = (population == 'user') ? 'user_metrics' : 'aggregate_metrics'; METRIC_LIST.forEach((metricName, i) => { metrics[metricName] = serverResponse[dataKey][i]; }); @@ -73,60 +75,49 @@ const MetricsTab = () => { } const { width: windowWidth } = useWindowDimensions(); - const cardWidth = windowWidth * 0.88; + const cardWidth = windowWidth * .88; - return ( - <> - - - - - - - - - - - - - - - - - - - - {/* + + + + + + + + + + + + + + + + + + + + {/* */} - - - - ); -}; + + + ); +} export const cardMargin = 10; @@ -143,7 +134,7 @@ export const cardStyles: any = { titleText: (colors) => ({ color: colors.onPrimary, fontWeight: '500', - textAlign: 'center', + textAlign: 'center' }), subtitleText: { fontSize: 13, @@ -155,8 +146,8 @@ export const cardStyles: any = { padding: 8, paddingBottom: 12, flex: 1, - }, -}; + } +} angularize(MetricsTab, 'MetricsTab', 'emission.main.metricstab'); export default MetricsTab; diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index 387ebc79d..99bf9d425 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -1,6 +1,7 @@ + import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme } from 'react-native-paper'; +import { Card, Text, useTheme} from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardMargin, cardStyles } from './MetricsTab'; import { formatDateRangeOfDays, segmentDaysByWeeks } from './metricsHelper'; @@ -10,70 +11,68 @@ import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHe import { getBaseModeByText } from '../diary/diaryHelper'; export const ACTIVE_MODES = ['walk', 'bike'] as const; -type ActiveMode = (typeof ACTIVE_MODES)[number]; +type ActiveMode = typeof ACTIVE_MODES[number]; -type Props = { userMetrics: MetricsData }; +type Props = { userMetrics: MetricsData } const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { + const { colors } = useTheme(); const { t } = useTranslation(); + const weeklyActiveMinutesRecords = useMemo(() => { const records = []; - const [recentWeek, prevWeek] = segmentDaysByWeeks(userMetrics?.duration, 2); - ACTIVE_MODES.forEach((mode) => { - const prevSum = prevWeek?.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); + const [ recentWeek, prevWeek ] = segmentDaysByWeeks(userMetrics?.duration, 2); + ACTIVE_MODES.forEach(mode => { + const prevSum = prevWeek?.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); if (prevSum) { const xLabel = `Previous Week\n(${formatDateRangeOfDays(prevWeek)})`; // TODO: i18n - records.push({ label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60 }); + records.push({label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60}); } - const recentSum = recentWeek?.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); + const recentSum = recentWeek?.reduce((acc, day) => ( + acc + (day[`label_${mode}`] || 0) + ), 0); if (recentSum) { const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n - records.push({ label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60 }); + records.push({label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60}); } }); - return records as { label: ActiveMode; x: string; y: number }[]; + return records as {label: ActiveMode, x: string, y: number}[]; }, [userMetrics?.duration]); return ( - - + + style={cardStyles.title(colors)} /> - {weeklyActiveMinutesRecords.length ? ( - - getBaseModeByText(l, labelOptions).color} - /> - + { weeklyActiveMinutesRecords.length ? + + getBaseModeByText(l, labelOptions).color} /> + {t('main-metrics.weekly-goal-footnote')} - ) : ( - - + : + + {t('metrics.chart-no-data')} - )} + } - ); -}; + ) +} export default WeeklyActiveMinutesCard; diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index 3df71cdc1..d1cd435d4 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,12 +1,12 @@ -import { DateTime } from 'luxon'; -import { formatForDisplay } from '../config/useImperialConfig'; -import { DayOfMetricData } from './metricsTypes'; +import { DateTime } from "luxon"; +import { formatForDisplay } from "../config/useImperialConfig"; +import { DayOfMetricData } from "./metricsTypes"; import moment from 'moment'; export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { const uniqueLabels: string[] = []; - metricDataDays.forEach((e) => { - Object.keys(e).forEach((k) => { + metricDataDays.forEach(e => { + Object.keys(e).forEach(k => { if (k.startsWith('label_')) { const label = k.substring(6); // remove 'label_' prefix leaving just the mode label if (!uniqueLabels.includes(label)) uniqueLabels.push(label); @@ -16,39 +16,42 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { return uniqueLabels; } -export const getLabelsForDay = (metricDataDay: DayOfMetricData) => +export const getLabelsForDay = (metricDataDay: DayOfMetricData) => ( Object.keys(metricDataDay).reduce((acc, k) => { if (k.startsWith('label_')) { acc.push(k.substring(6)); // remove 'label_' prefix leaving just the mode label } return acc; - }, [] as string[]); + }, [] as string[]) +); -export const secondsToMinutes = (seconds: number) => formatForDisplay(seconds / 60); +export const secondsToMinutes = (seconds: number) => + formatForDisplay(seconds / 60); -export const secondsToHours = (seconds: number) => formatForDisplay(seconds / 3600); +export const secondsToHours = (seconds: number) => + formatForDisplay(seconds / 3600); // segments metricsDays into weeks, with the most recent week first -export function segmentDaysByWeeks(days: DayOfMetricData[], nWeeks?: number) { +export function segmentDaysByWeeks (days: DayOfMetricData[], nWeeks?: number) { const weeks: DayOfMetricData[][] = []; for (let i = days?.length - 1; i >= 0; i -= 7) { weeks.push(days.slice(Math.max(i - 6, 0), i + 1)); } if (nWeeks) return weeks.slice(0, nWeeks); return weeks; -} +}; export function formatDate(day: DayOfMetricData) { const dt = DateTime.fromISO(day.fmt_time, { zone: 'utc' }); - return dt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined }); + return dt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); } export function formatDateRangeOfDays(days: DayOfMetricData[]) { if (!days?.length) return ''; const firstDayDt = DateTime.fromISO(days[0].fmt_time, { zone: 'utc' }); const lastDayDt = DateTime.fromISO(days[days.length - 1].fmt_time, { zone: 'utc' }); - const firstDay = firstDayDt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined }); - const lastDay = lastDayDt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined }); + const firstDay = firstDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); + const lastDay = lastDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); return `${firstDay} - ${lastDay}`; } @@ -58,49 +61,50 @@ export function formatDateRangeOfDays(days: DayOfMetricData[]) { const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; /* - * metric2val is a function that takes a metric entry and a field and returns - * the appropriate value. - * for regular data (user-specific), this will return the field value - * for avg data (aggregate), this will return the field value/nUsers - */ -const metricToValue = function (population: 'user' | 'aggreagte', metric, field) { - if (population == 'user') { +* metric2val is a function that takes a metric entry and a field and returns +* the appropriate value. +* for regular data (user-specific), this will return the field value +* for avg data (aggregate), this will return the field value/nUsers +*/ +const metricToValue = function(population:'user'|'aggreagte', metric, field) { + if(population == "user"){ return metric[field]; - } else { - return metric[field] / metric.nUsers; } -}; + else{ + return metric[field]/metric.nUsers; + } +} //testing agains global list of what is "on foot" //returns true | false -const isOnFoot = function (mode: string) { +const isOnFoot = function(mode: string) { for (let ped_mode in ON_FOOT_MODES) { if (mode === ped_mode) { return true; } } return false; -}; +} //from two weeks fo low and high values, calculates low and high change export function calculatePercentChange(pastWeekRange, previousWeekRange) { let greaterLesserPct = { - low: (pastWeekRange.low / previousWeekRange.low) * 100 - 100, - high: (pastWeekRange.high / previousWeekRange.high) * 100 - 100, - }; + low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, + high: (pastWeekRange.high/previousWeekRange.high) * 100 - 100, + } return greaterLesserPct; } export function parseDataFromMetrics(metrics, population) { - console.log('Called parseDataFromMetrics on ', metrics); + console.log("Called parseDataFromMetrics on ", metrics); let mode_bins = {}; - metrics?.forEach(function (metric) { + metrics?.forEach(function(metric) { let onFootVal = 0; for (let field in metric) { /*For modes inferred from sensor data, we check if the string is all upper case by converting it to upper case and seeing if it is changed*/ - if (field == field.toUpperCase()) { + if(field == field.toUpperCase()) { /*sum all possible on foot modes: see https://github.com/e-mission/e-mission-docs/issues/422 */ if (isOnFoot(field)) { onFootVal += metricToValue(population, metric, field); @@ -110,56 +114,49 @@ export function parseDataFromMetrics(metrics, population) { mode_bins[field] = []; } //for all except onFoot, add to bin - could discover mult onFoot modes - if (field != 'ON_FOOT') { - mode_bins[field].push([ - metric.ts, - metricToValue(population, metric, field), - metric.fmt_time, - ]); + if (field != "ON_FOOT") { + mode_bins[field].push([metric.ts, metricToValue(population, metric, field), metric.fmt_time]); } } //this section handles user lables, assuming 'label_' prefix - if (field.startsWith('label_')) { + if(field.startsWith('label_')) { let actualMode = field.slice(6, field.length); //remove prefix - console.log('Mapped field ' + field + ' to mode ' + actualMode); + console.log("Mapped field "+field+" to mode "+actualMode); if (!(actualMode in mode_bins)) { - mode_bins[actualMode] = []; + mode_bins[actualMode] = []; } - mode_bins[actualMode].push([ - metric.ts, - Math.round(metricToValue(population, metric, field)), - moment(metric.fmt_time).format(), - ]); + mode_bins[actualMode].push([metric.ts, Math.round(metricToValue(population, metric, field)), moment(metric.fmt_time).format()]); } } //handle the ON_FOOT modes once all have been summed - if ('ON_FOOT' in mode_bins) { - mode_bins['ON_FOOT'].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); + if ("ON_FOOT" in mode_bins) { + mode_bins["ON_FOOT"].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); } }); let return_val = []; for (let mode in mode_bins) { - return_val.push({ key: mode, values: mode_bins[mode] }); + return_val.push({key: mode, values: mode_bins[mode]}); } return return_val; } export function generateSummaryFromData(modeMap, metric) { - console.log('Invoked getSummaryDataRaw on ', modeMap, 'with', metric); + console.log("Invoked getSummaryDataRaw on ", modeMap, "with", metric); let summaryMap = []; - for (let i = 0; i < modeMap.length; i++) { + for (let i=0; i < modeMap.length; i++){ let summary = {}; - summary['key'] = modeMap[i].key; + summary['key'] = modeMap[i].key; let sumVals = 0; - for (let j = 0; j < modeMap[i].values.length; j++) { + for (let j = 0; j < modeMap[i].values.length; j++) + { sumVals += modeMap[i].values[j][1]; //2nd item of array is value } - if (metric === 'mean_speed') { + if (metric === 'mean_speed'){ //we care about avg speed, sum for other metrics summary['values'] = Math.round(sumVals / modeMap[i].values.length); } else { @@ -173,13 +170,13 @@ export function generateSummaryFromData(modeMap, metric) { } /* - * We use the results to determine whether these results are from custom - * labels or from the automatically sensed labels. Automatically sensedV - * labels are in all caps, custom labels are prefixed by label, but have had - * the label_prefix stripped out before this. Results should have either all - * sensed labels or all custom labels. - */ -export const isCustomLabels = function (modeMap) { +* We use the results to determine whether these results are from custom +* labels or from the automatically sensed labels. Automatically sensedV +* labels are in all caps, custom labels are prefixed by label, but have had +* the label_prefix stripped out before this. Results should have either all +* sensed labels or all custom labels. +*/ +export const isCustomLabels = function(modeMap) { const isSensed = (mode) => mode == mode.toUpperCase(); const isCustom = (mode) => mode == mode.toLowerCase(); const metricSummaryChecksCustom = []; @@ -188,34 +185,28 @@ export const isCustomLabels = function (modeMap) { const distanceKeys = modeMap.map((e) => e.key); const isSensedKeys = distanceKeys.map(isSensed); const isCustomKeys = distanceKeys.map(isCustom); - console.log( - 'Checking metric keys', - distanceKeys, - ' sensed ', - isSensedKeys, - ' custom ', - isCustomKeys, - ); + console.log("Checking metric keys", distanceKeys, " sensed ", isSensedKeys, + " custom ", isCustomKeys); const isAllCustomForMetric = isAllCustom(isSensedKeys, isCustomKeys); metricSummaryChecksSensed.push(!isAllCustomForMetric); metricSummaryChecksCustom.push(isAllCustomForMetric); - console.log('overall custom/not results for each metric = ', metricSummaryChecksCustom); + console.log("overall custom/not results for each metric = ", metricSummaryChecksCustom); return isAllCustom(metricSummaryChecksSensed, metricSummaryChecksCustom); -}; +} -const isAllCustom = function (isSensedKeys, isCustomKeys) { - const allSensed = isSensedKeys.reduce((a, b) => a && b, true); - const anySensed = isSensedKeys.reduce((a, b) => a || b, false); - const allCustom = isCustomKeys.reduce((a, b) => a && b, true); - const anyCustom = isCustomKeys.reduce((a, b) => a || b, false); - if (allSensed && !anyCustom) { - return false; // sensed, not custom - } - if (!anySensed && allCustom) { - return true; // custom, not sensed; false implies that the other option is true - } - // Logger.displayError("Mixed entries that combine sensed and custom labels", - // "Please report to your program admin"); - return undefined; -}; +const isAllCustom = function(isSensedKeys, isCustomKeys) { + const allSensed = isSensedKeys.reduce((a, b) => a && b, true); + const anySensed = isSensedKeys.reduce((a, b) => a || b, false); + const allCustom = isCustomKeys.reduce((a, b) => a && b, true); + const anyCustom = isCustomKeys.reduce((a, b) => a || b, false); + if ((allSensed && !anyCustom)) { + return false; // sensed, not custom + } + if ((!anySensed && allCustom)) { + return true; // custom, not sensed; false implies that the other option is true + } + // Logger.displayError("Mixed entries that combine sensed and custom labels", + // "Please report to your program admin"); + return undefined; +} \ No newline at end of file diff --git a/www/js/metrics/metricsTypes.ts b/www/js/metrics/metricsTypes.ts index cfe4444a3..d51c98b3a 100644 --- a/www/js/metrics/metricsTypes.ts +++ b/www/js/metrics/metricsTypes.ts @@ -1,14 +1,14 @@ -import { METRIC_LIST } from './MetricsTab'; +import { METRIC_LIST } from "./MetricsTab" -type MetricName = (typeof METRIC_LIST)[number]; -type LabelProps = { [k in `label_${string}`]?: number }; // label_, where could be anything +type MetricName = typeof METRIC_LIST[number]; +type LabelProps = {[k in `label_${string}`]?: number}; // label_, where could be anything export type DayOfMetricData = LabelProps & { - ts: number; - fmt_time: string; - nUsers: number; - local_dt: { [k: string]: any }; // TODO type datetime obj -}; + ts: number, + fmt_time: string, + nUsers: number, + local_dt: {[k: string]: any}, // TODO type datetime obj +} export type MetricsData = { - [key in MetricName]: DayOfMetricData[]; -}; + [key in MetricName]: DayOfMetricData[] +} diff --git a/www/js/plugin/logger.ts b/www/js/plugin/logger.ts index e8f62525d..c4e476de1 100644 --- a/www/js/plugin/logger.ts +++ b/www/js/plugin/logger.ts @@ -1,33 +1,28 @@ import angular from 'angular'; -angular - .module('emission.plugin.logger', []) +angular.module('emission.plugin.logger', []) - // explicit annotations needed in .ts files - Babel does not fix them (see webpack.prod.js) - .factory('Logger', [ - '$window', - '$ionicPopup', - function ($window, $ionicPopup) { - var loggerJs: any = {}; - loggerJs.log = function (message) { +// explicit annotations needed in .ts files - Babel does not fix them (see webpack.prod.js) +.factory('Logger', ['$window', '$ionicPopup', function($window, $ionicPopup) { + var loggerJs: any = {}; + loggerJs.log = function(message) { $window.Logger.log($window.Logger.LEVEL_DEBUG, message); - }; - loggerJs.displayError = function (title, error) { - var display_msg = error.message + '\n' + error.stack; - if (!angular.isDefined(error.message)) { - display_msg = JSON.stringify(error); - } - // Check for OPcode DNE errors and prepend the title with "Invalid OPcode" - if (error.includes?.('403') || error.message?.includes?.('403')) { - title = 'Invalid OPcode: ' + title; - } - $ionicPopup.alert({ title: title, template: display_msg }); - console.log(title + display_msg); - $window.Logger.log($window.Logger.LEVEL_ERROR, title + display_msg); - }; - return loggerJs; - }, - ]); + } + loggerJs.displayError = function(title, error) { + var display_msg = error.message + "\n" + error.stack; + if (!angular.isDefined(error.message)) { + display_msg = JSON.stringify(error); + } + // Check for OPcode DNE errors and prepend the title with "Invalid OPcode" + if (error.includes?.("403") || error.message?.includes?.("403")) { + title = "Invalid OPcode: " + title; + } + $ionicPopup.alert({"title": title, "template": display_msg}); + console.log(title + display_msg); + $window.Logger.log($window.Logger.LEVEL_ERROR, title + display_msg); + } + return loggerJs; +}]); export const logDebug = (message: string) => window['Logger'].log(window['Logger'].LEVEL_DEBUG, message); @@ -45,8 +40,8 @@ export function displayError(error: Error, title?: string) { export function displayErrorMsg(errorMsg: string, title?: string) { // Check for OPcode 'Does Not Exist' errors and prepend the title with "Invalid OPcode" - if (errorMsg.includes?.('403')) { - title = 'Invalid OPcode: ' + (title || ''); + if (errorMsg.includes?.("403")) { + title = "Invalid OPcode: " + (title || ''); } const displayMsg = `━━━━\n${title}\n━━━━\n` + errorMsg; window.alert(displayMsg); diff --git a/www/js/plugin/storage.js b/www/js/plugin/storage.js index 02a04dd13..a14b1db83 100644 --- a/www/js/plugin/storage.js +++ b/www/js/plugin/storage.js @@ -1,41 +1,38 @@ import angular from 'angular'; -angular - .module('emission.plugin.kvstore', [ - 'emission.plugin.logger', - 'LocalStorageModule', - 'emission.stats.clientstats', - ]) - - .factory( - 'KVStore', - function ($window, Logger, localStorageService, $ionicPopup, $ionicPlatform, ClientStats) { - var logger = Logger; - var kvstoreJs = {}; - /* - * Sets in both localstorage and native storage - * If the message is not a JSON object, wrap it in an object with the key - * "value" before storing it. - */ - var getNativePlugin = function () { +angular.module('emission.plugin.kvstore', ['emission.plugin.logger', + 'LocalStorageModule', + 'emission.stats.clientstats']) + +.factory('KVStore', function($window, Logger, localStorageService, $ionicPopup, + $ionicPlatform, ClientStats) { + var logger = Logger; + var kvstoreJs = {} + /* + * Sets in both localstorage and native storage + * If the message is not a JSON object, wrap it in an object with the key + * "value" before storing it. + */ + var getNativePlugin = function() { return $window.cordova.plugins.BEMUserCache; - }; + } - /* - * Munge plain, non-JSON objects to JSON objects before storage - */ + /* + * Munge plain, non-JSON objects to JSON objects before storage + */ - var mungeValue = function (key, value) { + var mungeValue = function(key, value) { var store_val = value; - if (typeof value != 'object') { - // Should this be {"value": value} or {key: value}? - store_val = {}; - store_val[key] = value; + if (typeof value != "object") { + // Should this be {"value": value} or {key: value}? + store_val = {}; + store_val[key] = value; } return store_val; - }; + } - kvstoreJs.set = function (key, value) { + + kvstoreJs.set = function(key, value) { // add checks for data type var store_val = mungeValue(key, value); /* @@ -46,132 +43,77 @@ angular */ localStorageService.set(key, store_val); return getNativePlugin().putLocalStorage(key, store_val); - }; + } - var getUnifiedValue = function (key) { + var getUnifiedValue = function(key) { var ls_stored_val = localStorageService.get(key, undefined); - return getNativePlugin() - .getLocalStorage(key, false) - .then(function (uc_stored_val) { - logger.log( - 'for key ' + - key + - ' uc_stored_val = ' + - JSON.stringify(uc_stored_val) + - ' ls_stored_val = ' + - JSON.stringify(ls_stored_val), - ); + return getNativePlugin().getLocalStorage(key, false).then(function(uc_stored_val) { + logger.log("for key "+key+" uc_stored_val = "+JSON.stringify(uc_stored_val)+" ls_stored_val = "+JSON.stringify(ls_stored_val)); if (angular.equals(ls_stored_val, uc_stored_val)) { - logger.log('local and native values match, already synced'); - return uc_stored_val; + logger.log("local and native values match, already synced"); + return uc_stored_val; } else { - // the values are different - if (ls_stored_val == null) { - console.assert(uc_stored_val != null, 'uc_stored_val should be non-null'); - logger.log( - 'for key ' + - key + - 'uc_stored_val = ' + - JSON.stringify(uc_stored_val) + - ' ls_stored_val = ' + - JSON.stringify(ls_stored_val) + - ' copying native ' + - key + - ' to local...', - ); + // the values are different + if (ls_stored_val == null) { + console.assert(uc_stored_val != null, "uc_stored_val should be non-null"); + logger.log("for key "+key+"uc_stored_val = "+JSON.stringify(uc_stored_val)+ + " ls_stored_val = "+JSON.stringify(ls_stored_val)+ + " copying native "+key+" to local..."); + localStorageService.set(key, uc_stored_val); + return uc_stored_val; + } else if (uc_stored_val == null) { + console.assert(ls_stored_val != null); + /* + * Backwards compatibility ONLY. Right after the first + * update to this version, we may have a local value that + * is not a JSON object. In that case, we want to munge it + * before storage. Remove this after a few releases. + */ + ls_stored_val = mungeValue(key, ls_stored_val); + $ionicPopup.alert({template: "Local "+key+" found, native " + +key+" missing, writing "+key+" to native"}) + logger.log("for key "+key+"uc_stored_val = "+JSON.stringify(uc_stored_val)+ + " ls_stored_val = "+JSON.stringify(ls_stored_val)+ + " copying local "+key+" to native..."); + return getNativePlugin().putLocalStorage(key, ls_stored_val).then(function() { + // we only return the value after we have finished writing + return ls_stored_val; + }); + } + console.assert(ls_stored_val != null && uc_stored_val != null, + "ls_stored_val ="+JSON.stringify(ls_stored_val)+ + "uc_stored_val ="+JSON.stringify(uc_stored_val)); + $ionicPopup.alert({template: "Local "+key+" found, native " + +key+" found, but different, writing "+key+" to local"}) + logger.log("for key "+key+"uc_stored_val = "+JSON.stringify(uc_stored_val)+ + " ls_stored_val = "+JSON.stringify(ls_stored_val)+ + " copying native "+key+" to local..."); localStorageService.set(key, uc_stored_val); return uc_stored_val; - } else if (uc_stored_val == null) { - console.assert(ls_stored_val != null); - /* - * Backwards compatibility ONLY. Right after the first - * update to this version, we may have a local value that - * is not a JSON object. In that case, we want to munge it - * before storage. Remove this after a few releases. - */ - ls_stored_val = mungeValue(key, ls_stored_val); - $ionicPopup.alert({ - template: - 'Local ' + - key + - ' found, native ' + - key + - ' missing, writing ' + - key + - ' to native', - }); - logger.log( - 'for key ' + - key + - 'uc_stored_val = ' + - JSON.stringify(uc_stored_val) + - ' ls_stored_val = ' + - JSON.stringify(ls_stored_val) + - ' copying local ' + - key + - ' to native...', - ); - return getNativePlugin() - .putLocalStorage(key, ls_stored_val) - .then(function () { - // we only return the value after we have finished writing - return ls_stored_val; - }); - } - console.assert( - ls_stored_val != null && uc_stored_val != null, - 'ls_stored_val =' + - JSON.stringify(ls_stored_val) + - 'uc_stored_val =' + - JSON.stringify(uc_stored_val), - ); - $ionicPopup.alert({ - template: - 'Local ' + - key + - ' found, native ' + - key + - ' found, but different, writing ' + - key + - ' to local', - }); - logger.log( - 'for key ' + - key + - 'uc_stored_val = ' + - JSON.stringify(uc_stored_val) + - ' ls_stored_val = ' + - JSON.stringify(ls_stored_val) + - ' copying native ' + - key + - ' to local...', - ); - localStorageService.set(key, uc_stored_val); - return uc_stored_val; } - }); - }; + }); + } - /* - * If a non-JSON object was munged for storage, unwrap it. - */ - var unmungeValue = function (key, retData) { - if (retData != null && angular.isDefined(retData[key])) { - // it must have been a simple data type that we munged upfront - return retData[key]; + /* + * If a non-JSON object was munged for storage, unwrap it. + */ + var unmungeValue = function(key, retData) { + if((retData != null) && (angular.isDefined(retData[key]))) { + // it must have been a simple data type that we munged upfront + return retData[key]; } else { - // it must have been an object - return retData; + // it must have been an object + return retData; } - }; + } - kvstoreJs.get = function (key) { - return getUnifiedValue(key).then(function (retData) { - return unmungeValue(key, retData); + kvstoreJs.get = function(key) { + return getUnifiedValue(key).then(function(retData) { + return unmungeValue(key, retData); }); - }; + } - /* + /* * TODO: Temporary fix for data that: - we want to return inline instead of in a promise - is not catastrophic if it is cleared out (e.g. walkthrough code), OR @@ -182,110 +124,98 @@ angular The code does copy the native value to local storage in the background, so even if this is stripped out, it will work on retry. */ - kvstoreJs.getDirect = function (key) { + kvstoreJs.getDirect = function(key) { // will run in background, we won't wait for the results getUnifiedValue(key); return unmungeValue(key, localStorageService.get(key)); - }; + } - kvstoreJs.remove = function (key) { + kvstoreJs.remove = function(key) { localStorageService.remove(key); return getNativePlugin().removeLocalStorage(key); - }; + } - kvstoreJs.clearAll = function () { + kvstoreJs.clearAll = function() { localStorageService.clearAll(); return getNativePlugin().clearAll(); - }; + } - /* - * Unfortunately, there is weird deletion of native - * https://github.com/e-mission/e-mission-docs/issues/930 - * So we cannot remove this if/until we switch to react native - */ - kvstoreJs.clearOnlyLocal = function () { + /* + * Unfortunately, there is weird deletion of native + * https://github.com/e-mission/e-mission-docs/issues/930 + * So we cannot remove this if/until we switch to react native + */ + kvstoreJs.clearOnlyLocal = function() { return localStorageService.clearAll(); - }; + } - kvstoreJs.clearOnlyNative = function () { + kvstoreJs.clearOnlyNative = function() { return getNativePlugin().clearAll(); - }; + } - let findMissing = function (fromKeys, toKeys) { + let findMissing = function(fromKeys, toKeys) { const foundKeys = []; const missingKeys = []; fromKeys.forEach((fk) => { - if (toKeys.includes(fk)) { - foundKeys.push(fk); - } else { - missingKeys.push(fk); - } + if (toKeys.includes(fk)) { + foundKeys.push(fk); + } else { + missingKeys.push(fk); + } }); return [foundKeys, missingKeys]; - }; + } - let syncAllWebAndNativeValues = function () { - console.log('STORAGE_PLUGIN: Called syncAllWebAndNativeValues '); - const syncKeys = getNativePlugin() - .listAllLocalStorageKeys() - .then((nativeKeys) => { - console.log('STORAGE_PLUGIN: native plugin returned'); + let syncAllWebAndNativeValues = function() { + console.log("STORAGE_PLUGIN: Called syncAllWebAndNativeValues "); + const syncKeys = getNativePlugin().listAllLocalStorageKeys().then((nativeKeys) => { + console.log("STORAGE_PLUGIN: native plugin returned"); const webKeys = localStorageService.keys(); // I thought about iterating through the lists and copying over // only missing values, etc but `getUnifiedValue` already does // that, and we don't need to copy it // so let's just find all the missing values and read them - logger.log('STORAGE_PLUGIN: Comparing web keys ' + webKeys + ' with ' + nativeKeys); + logger.log("STORAGE_PLUGIN: Comparing web keys "+webKeys+" with "+nativeKeys); let [foundNative, missingNative] = findMissing(webKeys, nativeKeys); let [foundWeb, missingWeb] = findMissing(nativeKeys, webKeys); - logger.log( - 'STORAGE_PLUGIN: Found native keys ' + - foundNative + - ' missing native keys ' + - missingNative, - ); - logger.log( - 'STORAGE_PLUGIN: Found web keys ' + foundWeb + ' missing web keys ' + missingWeb, - ); + logger.log("STORAGE_PLUGIN: Found native keys "+foundNative+" missing native keys "+missingNative); + logger.log("STORAGE_PLUGIN: Found web keys "+foundWeb+" missing web keys "+missingWeb); const allMissing = missingNative.concat(missingWeb); - logger.log('STORAGE_PLUGIN: Syncing all missing keys ' + allMissing); + logger.log("STORAGE_PLUGIN: Syncing all missing keys "+allMissing); allMissing.forEach(getUnifiedValue); if (allMissing.length != 0) { - ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { - type: 'local_storage_mismatch', - allMissingLength: allMissing.length, - missingWebLength: missingWeb.length, - missingNativeLength: missingNative.length, - foundWebLength: foundWeb.length, - foundNativeLength: foundNative.length, - allMissing: allMissing, - }).then(Logger.log('Logged missing keys to client stats')); + ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { + "type": "local_storage_mismatch", + "allMissingLength": allMissing.length, + "missingWebLength": missingWeb.length, + "missingNativeLength": missingNative.length, + "foundWebLength": foundWeb.length, + "foundNativeLength": foundNative.length, + "allMissing": allMissing, + }).then(Logger.log("Logged missing keys to client stats")); } - }); - const listAllKeys = getNativePlugin() - .listAllUniqueKeys() - .then((nativeKeys) => { - logger.log('STORAGE_PLUGIN: For the record, all unique native keys are ' + nativeKeys); + }); + const listAllKeys = getNativePlugin().listAllUniqueKeys().then((nativeKeys) => { + logger.log("STORAGE_PLUGIN: For the record, all unique native keys are "+nativeKeys); if (nativeKeys.length == 0) { - ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { - type: 'all_native', - }).then(Logger.log('Logged all missing native keys to client stats')); + ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { + "type": "all_native", + }).then(Logger.log("Logged all missing native keys to client stats")); } - }); + }); return Promise.all([syncKeys, listAllKeys]); - }; + } - $ionicPlatform.ready().then(function () { - Logger.log('STORAGE_PLUGIN: app launched, checking storage sync'); + $ionicPlatform.ready().then(function() { + Logger.log("STORAGE_PLUGIN: app launched, checking storage sync"); syncAllWebAndNativeValues(); - }); + }); - $ionicPlatform.on('resume', function () { - Logger.log('STORAGE_PLUGIN: app has resumed, checking storage sync'); + $ionicPlatform.on("resume", function() { + Logger.log("STORAGE_PLUGIN: app has resumed, checking storage sync"); syncAllWebAndNativeValues(); - }); + }); - return kvstoreJs; - }, - ); + return kvstoreJs; +}); diff --git a/www/js/services.js b/www/js/services.js index 470c4774e..9a63b364d 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -2,115 +2,94 @@ import angular from 'angular'; -angular - .module('emission.services', ['emission.plugin.logger', 'emission.plugin.kvstore']) - - .service('CommHelper', function ($rootScope) { - var getConnectURL = function (successCallback, errorCallback) { - window.cordova.plugins.BEMConnectionSettings.getSettings(function (settings) { - successCallback(settings.connectUrl); - }, errorCallback); +angular.module('emission.services', ['emission.plugin.logger', + 'emission.plugin.kvstore']) + +.service('CommHelper', function($rootScope) { + var getConnectURL = function(successCallback, errorCallback) { + window.cordova.plugins.BEMConnectionSettings.getSettings( + function(settings) { + successCallback(settings.connectUrl); + }, errorCallback); }; - var processErrorMessages = function (errorMsg) { - if (errorMsg.includes('403')) { - errorMsg = 'Error: OPcode does not exist on the server. ' + errorMsg; - console.error('Error 403 found. ' + errorMsg); + var processErrorMessages = function(errorMsg) { + if (errorMsg.includes("403")) { + errorMsg = "Error: OPcode does not exist on the server. " + errorMsg; + console.error("Error 403 found. " + errorMsg); } return errorMsg; - }; + } - this.registerUser = function (successCallback, errorCallback) { - window.cordova.plugins.BEMServerComm.getUserPersonalData( - '/profile/create', - successCallback, - errorCallback, - ); + this.registerUser = function(successCallback, errorCallback) { + window.cordova.plugins.BEMServerComm.getUserPersonalData("/profile/create", successCallback, errorCallback); }; - this.updateUser = function (updateDoc) { - return new Promise(function (resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData( - '/profile/update', - 'update_doc', - updateDoc, - resolve, - reject, - ); - }).catch((error) => { - error = 'While updating user, ' + error; - error = processErrorMessages(error); - throw error; - }); + this.updateUser = function(updateDoc) { + return new Promise(function(resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData("/profile/update", "update_doc", updateDoc, resolve, reject); + }) + .catch(error => { + error = "While updating user, " + error; + error = processErrorMessages(error); + throw(error); + }); }; - this.getUser = function () { - return new Promise(function (resolve, reject) { - window.cordova.plugins.BEMServerComm.getUserPersonalData('/profile/get', resolve, reject); - }).catch((error) => { - error = 'While getting user, ' + error; - error = processErrorMessages(error); - throw error; - }); + this.getUser = function() { + return new Promise(function(resolve, reject) { + window.cordova.plugins.BEMServerComm.getUserPersonalData("/profile/get", resolve, reject); + }) + .catch(error => { + error = "While getting user, " + error; + error = processErrorMessages(error); + throw(error); + }); }; - this.putOne = function (key, data) { - var now = moment().unix(); - var md = { - write_ts: now, - read_ts: now, - time_zone: moment.tz.guess(), - type: 'message', - key: key, - platform: ionic.Platform.platform(), - }; - var entryToPut = { - metadata: md, - data: data, - }; - return new Promise(function (resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData( - '/usercache/putone', - 'the_entry', - entryToPut, - resolve, - reject, - ); - }).catch((error) => { - error = 'While putting one entry, ' + error; - error = processErrorMessages(error); - throw error; - }); + this.putOne = function(key, data) { + var now = moment().unix(); + var md = { + "write_ts": now, + "read_ts": now, + "time_zone": moment.tz.guess(), + "type": "message", + "key": key, + "platform": ionic.Platform.platform() + }; + var entryToPut = { + "metadata": md, + "data": data + } + return new Promise(function(resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData("/usercache/putone", "the_entry", entryToPut, resolve, reject); + }) + .catch(error => { + error = "While putting one entry, " + error; + error = processErrorMessages(error); + throw(error); + }); }; - this.getTimelineForDay = function (date) { - return new Promise(function (resolve, reject) { - var dateString = date.startOf('day').format('YYYY-MM-DD'); - window.cordova.plugins.BEMServerComm.getUserPersonalData( - '/timeline/getTrips/' + dateString, - resolve, - reject, - ); - }).catch((error) => { - error = 'While getting timeline for day, ' + error; - error = processErrorMessages(error); - throw error; - }); + this.getTimelineForDay = function(date) { + return new Promise(function(resolve, reject) { + var dateString = date.startOf('day').format('YYYY-MM-DD'); + window.cordova.plugins.BEMServerComm.getUserPersonalData("/timeline/getTrips/"+dateString, resolve, reject); + }) + .catch(error => { + error = "While getting timeline for day, " + error; + error = processErrorMessages(error); + throw(error); + }); }; /* * var regConfig = {'username': ....} * Other fields can be added easily and the server can be modified at the same time. */ - this.habiticaRegister = function (regConfig) { - return new Promise(function (resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData( - '/habiticaRegister', - 'regConfig', - regConfig, - resolve, - reject, - ); + this.habiticaRegister = function(regConfig) { + return new Promise(function(resolve, reject){ + window.cordova.plugins.BEMServerComm.postUserPersonalData("/habiticaRegister", "regConfig", regConfig, resolve, reject); }); }; @@ -131,39 +110,30 @@ angular * .... */ - this.habiticaProxy = function (callOpts) { - return new Promise(function (resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData( - '/habiticaProxy', - 'callOpts', - callOpts, - resolve, - reject, - ); - }).catch((error) => { - error = 'While habitica proxy, ' + error; + this.habiticaProxy = function(callOpts){ + return new Promise(function(resolve, reject){ + window.cordova.plugins.BEMServerComm.postUserPersonalData("/habiticaProxy", "callOpts", callOpts, resolve, reject); + }) + .catch(error => { + error = "While habitica proxy, " + error; error = processErrorMessages(error); - throw error; + throw(error); }); }; - this.getMetrics = function (timeType, metrics_query) { - return new Promise(function (resolve, reject) { - var msgFiller = function (message) { - for (var key in metrics_query) { - message[key] = metrics_query[key]; - } + this.getMetrics = function(timeType, metrics_query) { + return new Promise(function(resolve, reject) { + var msgFiller = function(message) { + for (var key in metrics_query) { + message[key] = metrics_query[key] + }; }; - window.cordova.plugins.BEMServerComm.pushGetJSON( - '/result/metrics/' + timeType, - msgFiller, - resolve, - reject, - ); - }).catch((error) => { - error = 'While getting metrics, ' + error; + window.cordova.plugins.BEMServerComm.pushGetJSON("/result/metrics/"+timeType, msgFiller, resolve, reject); + }) + .catch(error => { + error = "While getting metrics, " + error; error = processErrorMessages(error); - throw error; + throw(error); }); }; @@ -172,195 +142,151 @@ angular * start_time = beginning timestamp for range * end_time = ending timestamp for rangeA */ - this.moment2Localdate = function (momentObj) { - return { - year: momentObj.year(), - month: momentObj.month() + 1, - day: momentObj.date(), - }; + this.moment2Localdate = function(momentObj) { + return { + year: momentObj.year(), + month: momentObj.month() + 1, + day: momentObj.date(), + }; }; - this.moment2Timestamp = function (momentObj) { + this.moment2Timestamp = function(momentObj) { return momentObj.unix(); - }; + } // time_key is typically metadata.write_ts or data.ts - this.getRawEntriesForLocalDate = function ( - key_list, - start_ts, - end_ts, - time_key = 'metadata.write_ts', - max_entries = undefined, - trunc_method = 'sample', - ) { - return new Promise(function (resolve, reject) { - var msgFiller = function (message) { - message.key_list = key_list; - message.from_local_date = moment2Localdate(moment.unix(start_ts)); - message.to_local_date = moment2Localdate(moment.unix(end_ts)); - message.key_local_date = time_key; - if (max_entries !== undefined) { - message.max_entries = max_entries; - message.trunc_method = trunc_method; - } - console.log('About to return message ' + JSON.stringify(message)); - }; - console.log('getRawEntries: about to get pushGetJSON for the timestamp'); - window.cordova.plugins.BEMServerComm.pushGetJSON( - '/datastreams/find_entries/local_date', - msgFiller, - resolve, - reject, - ); - }).catch((error) => { - error = 'While getting raw entries for local date, ' + error; - error = processErrorMessages(error); - throw error; + this.getRawEntriesForLocalDate = function(key_list, start_ts, end_ts, + time_key = "metadata.write_ts", max_entries = undefined, trunc_method = "sample") { + return new Promise(function(resolve, reject) { + var msgFiller = function(message) { + message.key_list = key_list; + message.from_local_date = moment2Localdate(moment.unix(start_ts)); + message.to_local_date = moment2Localdate(moment.unix(end_ts)); + message.key_local_date = time_key; + if (max_entries !== undefined) { + message.max_entries = max_entries; + message.trunc_method = trunc_method; + } + console.log("About to return message "+JSON.stringify(message)); + }; + console.log("getRawEntries: about to get pushGetJSON for the timestamp"); + window.cordova.plugins.BEMServerComm.pushGetJSON("/datastreams/find_entries/local_date", msgFiller, resolve, reject); + }) + .catch(error => { + error = "While getting raw entries for local date, " + error; + error = processErrorMessages(error); + throw(error); }); }; - this.getRawEntries = function ( - key_list, - start_ts, - end_ts, - time_key = 'metadata.write_ts', - max_entries = undefined, - trunc_method = 'sample', - ) { - return new Promise(function (resolve, reject) { - var msgFiller = function (message) { - message.key_list = key_list; - message.start_time = start_ts; - message.end_time = end_ts; - message.key_time = time_key; - if (max_entries !== undefined) { - message.max_entries = max_entries; - message.trunc_method = trunc_method; - } - console.log('About to return message ' + JSON.stringify(message)); - }; - console.log('getRawEntries: about to get pushGetJSON for the timestamp'); - window.cordova.plugins.BEMServerComm.pushGetJSON( - '/datastreams/find_entries/timestamp', - msgFiller, - resolve, - reject, - ); - }).catch((error) => { - error = 'While getting raw entries, ' + error; - error = processErrorMessages(error); - throw error; + this.getRawEntries = function(key_list, start_ts, end_ts, + time_key = "metadata.write_ts", max_entries = undefined, trunc_method = "sample") { + return new Promise(function(resolve, reject) { + var msgFiller = function(message) { + message.key_list = key_list; + message.start_time = start_ts; + message.end_time = end_ts; + message.key_time = time_key; + if (max_entries !== undefined) { + message.max_entries = max_entries; + message.trunc_method = trunc_method; + } + console.log("About to return message "+JSON.stringify(message)); + }; + console.log("getRawEntries: about to get pushGetJSON for the timestamp"); + window.cordova.plugins.BEMServerComm.pushGetJSON("/datastreams/find_entries/timestamp", msgFiller, resolve, reject); + }) + .catch(error => { + error = "While getting raw entries, " + error; + error = processErrorMessages(error); + throw(error); }); }; - this.getPipelineCompleteTs = function () { - return new Promise(function (resolve, reject) { - console.log('getting pipeline complete timestamp'); - window.cordova.plugins.BEMServerComm.getUserPersonalData( - '/pipeline/get_complete_ts', - resolve, - reject, - ); - }).catch((error) => { - error = 'While getting pipeline complete timestamp, ' + error; - error = processErrorMessages(error); - throw error; + this.getPipelineCompleteTs = function() { + return new Promise(function(resolve, reject) { + console.log("getting pipeline complete timestamp"); + window.cordova.plugins.BEMServerComm.getUserPersonalData("/pipeline/get_complete_ts", resolve, reject); + }) + .catch(error => { + error = "While getting pipeline complete timestamp, " + error; + error = processErrorMessages(error); + throw(error); }); }; - this.getPipelineRangeTs = function () { - return new Promise(function (resolve, reject) { - console.log('getting pipeline range timestamps'); - window.cordova.plugins.BEMServerComm.getUserPersonalData( - '/pipeline/get_range_ts', - resolve, - reject, - ); - }).catch((error) => { - error = 'While getting pipeline range timestamps, ' + error; - error = processErrorMessages(error); - throw error; + this.getPipelineRangeTs = function() { + return new Promise(function(resolve, reject) { + console.log("getting pipeline range timestamps"); + window.cordova.plugins.BEMServerComm.getUserPersonalData("/pipeline/get_range_ts", resolve, reject); + }) + .catch(error => { + error = "While getting pipeline range timestamps, " + error; + error = processErrorMessages(error); + throw(error); }); }; + // host is automatically read from $rootScope.connectUrl, which is set in app.js - this.getAggregateData = function (path, data) { - return new Promise(function (resolve, reject) { - const full_url = $rootScope.connectUrl + '/' + path; - data['aggregate'] = true; - - if ($rootScope.aggregateAuth === 'no_auth') { - console.log( - 'getting aggregate data without user authentication from ' + - full_url + - ' with arguments ' + - JSON.stringify(data), - ); - const options = { - method: 'post', - data: data, - responseType: 'json', - }; - cordova.plugin.http.sendRequest( - full_url, - options, - function (response) { - resolve(response.data); - }, - function (error) { - reject(error); - }, - ); - } else { - console.log( - 'getting aggregate data with user authentication from ' + - full_url + - ' with arguments ' + - JSON.stringify(data), - ); - var msgFiller = function (message) { - return Object.assign(message, data); - }; - window.cordova.plugins.BEMServerComm.pushGetJSON('/' + path, msgFiller, resolve, reject); - } - }).catch((error) => { - error = 'While getting aggregate data, ' + error; - error = processErrorMessages(error); - throw error; - }); - }; - }) - - .service('ReferHelper', function ($http) { - this.habiticaRegister = function (groupid, successCallback, errorCallback) { - window.cordova.plugins.BEMServerComm.getUserPersonalData( - '/join.group/' + groupid, - successCallback, - errorCallback, - ); + this.getAggregateData = function(path, data) { + return new Promise(function(resolve, reject) { + const full_url = $rootScope.connectUrl+"/"+path; + data["aggregate"] = true + + if ($rootScope.aggregateAuth === "no_auth") { + console.log("getting aggregate data without user authentication from " + + full_url +" with arguments "+JSON.stringify(data)); + const options = { + method: 'post', + data: data, + responseType: 'json' + } + cordova.plugin.http.sendRequest(full_url, options, + function(response) { + resolve(response.data); + }, function(error) { + reject(error); + }); + } else { + console.log("getting aggregate data with user authentication from " + + full_url +" with arguments "+JSON.stringify(data)); + var msgFiller = function(message) { + return Object.assign(message, data); + }; + window.cordova.plugins.BEMServerComm.pushGetJSON("/"+path, msgFiller, resolve, reject); + } + }) + .catch(error => { + error = "While getting aggregate data, " + error; + error = processErrorMessages(error); + throw(error); + }); }; - this.joinGroup = function (groupid, userid) { - // TODO: - return new Promise(function (resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData( - '/join.group/' + groupid, - 'inviter', - userid, - resolve, - reject, - ); - }); +}) - //function firstUpperCase(string) { - // return string[0].toUpperCase() + string.slice(1); - //}*/ +.service('ReferHelper', function($http) { + + this.habiticaRegister = function(groupid, successCallback, errorCallback) { + window.cordova.plugins.BEMServerComm.getUserPersonalData("/join.group/"+groupid, successCallback, errorCallback); }; - }) - .service('UnifiedDataLoader', function ($window, CommHelper, Logger) { - var combineWithDedup = function (list1, list2) { + this.joinGroup = function(groupid, userid) { + + // TODO: + return new Promise(function(resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData("/join.group/"+groupid, "inviter", userid, resolve, reject); + }) + + //function firstUpperCase(string) { + // return string[0].toUpperCase() + string.slice(1); + //}*/ + } +}) +.service('UnifiedDataLoader', function($window, CommHelper, 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 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; @@ -368,296 +294,260 @@ angular }; // 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); + 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); - }); - }; + 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 = CommHelper.getRawEntries([key], tq.startTs, tq.endTs).then( - function (serverResponse) { - return serverResponse.phone_data; - }, - ); - return combinedPromise(localPromise, remotePromise, combineWithDedup); + this.getUnifiedSensorDataForInterval = function(key, tq) { + var localPromise = $window.cordova.plugins.BEMUserCache.getSensorDataForInterval(key, tq, true); + var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs) + .then(function(serverResponse) { + return serverResponse.phone_data; + }); + return combinedPromise(localPromise, remotePromise, combineWithDedup); }; - this.getUnifiedMessagesForInterval = function (key, tq, withMetadata) { + this.getUnifiedMessagesForInterval = function(key, tq, withMetadata) { var localPromise = $window.cordova.plugins.BEMUserCache.getMessagesForInterval(key, tq, true); - var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs).then( - function (serverResponse) { - return serverResponse.phone_data; - }, - ); + var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs) + .then(function(serverResponse) { + return serverResponse.phone_data; + }); return combinedPromise(localPromise, remotePromise, combineWithDedup); - }; - }) - .service('ControlHelper', function ($window, $ionicPopup, CommHelper, Logger) { - this.writeFile = function (fileEntry, resultList) { + } +}) +.service('ControlHelper', function($window, + $ionicPopup, + CommHelper, + Logger) { + + this.writeFile = function(fileEntry, resultList) { // Create a FileWriter object for our FileEntry (log.txt). - }; + } + + this.getMyData = function(startTs) { + var fmt = "YYYY-MM-DD"; + // We are only retrieving data for a single day to avoid + // running out of memory on the phone + var startMoment = moment(startTs); + var endMoment = moment(startTs).endOf("day"); + var dumpFile = startMoment.format(fmt) + "." + + endMoment.format(fmt) + + ".timeline"; + alert("Going to retrieve data to "+dumpFile); + + var writeDumpFile = function(result) { + return new Promise(function(resolve, reject) { + var resultList = result.phone_data; + window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function(fs) { + console.log('file system open: ' + fs.name); + fs.root.getFile(dumpFile, { create: true, exclusive: false }, function (fileEntry) { + console.log("fileEntry "+fileEntry.nativeURL+" is file?" + fileEntry.isFile.toString()); + fileEntry.createWriter(function (fileWriter) { + fileWriter.onwriteend = function() { + console.log("Successful file write..."); + resolve(); + // readFile(fileEntry); + }; + + fileWriter.onerror = function (e) { + console.log("Failed file write: " + e.toString()); + reject(); + }; - this.getMyData = function (startTs) { - var fmt = 'YYYY-MM-DD'; - // We are only retrieving data for a single day to avoid - // running out of memory on the phone - var startMoment = moment(startTs); - var endMoment = moment(startTs).endOf('day'); - var dumpFile = startMoment.format(fmt) + '.' + endMoment.format(fmt) + '.timeline'; - alert('Going to retrieve data to ' + dumpFile); - - var writeDumpFile = function (result) { - return new Promise(function (resolve, reject) { - var resultList = result.phone_data; - window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function (fs) { - console.log('file system open: ' + fs.name); - fs.root.getFile(dumpFile, { create: true, exclusive: false }, function (fileEntry) { - console.log( - 'fileEntry ' + fileEntry.nativeURL + ' is file?' + fileEntry.isFile.toString(), - ); - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function () { - console.log('Successful file write...'); - resolve(); - // readFile(fileEntry); - }; - - fileWriter.onerror = function (e) { - console.log('Failed file write: ' + e.toString()); - reject(); - }; - - // If data object is not passed in, - // create a new Blob instead. - var dataObj = new Blob([JSON.stringify(resultList, null, 2)], { - type: 'application/json', + // If data object is not passed in, + // create a new Blob instead. + var dataObj = new Blob([JSON.stringify(resultList, null, 2)], + { type: 'application/json' }); + fileWriter.write(dataObj); + }); + // this.writeFile(fileEntry, resultList); }); - fileWriter.write(dataObj); }); - // this.writeFile(fileEntry, resultList); }); - }); - }); - }; - - var emailData = function (result) { - return new Promise(function (resolve, reject) { - window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function (fs) { - console.log('During email, file system open: ' + fs.name); - fs.root.getFile(dumpFile, null, function (fileEntry) { - console.log( - 'fileEntry ' + fileEntry.nativeURL + ' is file?' + fileEntry.isFile.toString(), - ); - fileEntry.file( - function (file) { - var reader = new FileReader(); - - reader.onloadend = function () { - console.log('Successful file read with ' + this.result.length + ' characters'); - var dataArray = JSON.parse(this.result); - console.log('Successfully read resultList of size ' + dataArray.length); - // displayFileData(fileEntry.fullPath + ": " + this.result); - var attachFile = fileEntry.nativeURL; - if (ionic.Platform.isAndroid()) { - // At least on nexus, getting a temporary file puts it into - // the cache, so I can hardcode that for now - attachFile = 'app://cache/' + dumpFile; - } - if (ionic.Platform.isIOS()) { - alert(i18next.t('email-service.email-account-mail-app')); + } + + + var emailData = function(result) { + return new Promise(function(resolve, reject) { + window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function(fs) { + console.log("During email, file system open: "+fs.name); + fs.root.getFile(dumpFile, null, function(fileEntry) { + console.log("fileEntry "+fileEntry.nativeURL+" is file?"+fileEntry.isFile.toString()); + fileEntry.file(function (file) { + var reader = new FileReader(); + + reader.onloadend = function() { + console.log("Successful file read with " + this.result.length +" characters"); + var dataArray = JSON.parse(this.result); + console.log("Successfully read resultList of size "+dataArray.length); + // displayFileData(fileEntry.fullPath + ": " + this.result); + var attachFile = fileEntry.nativeURL; + if (ionic.Platform.isAndroid()) { + // At least on nexus, getting a temporary file puts it into + // the cache, so I can hardcode that for now + attachFile = "app://cache/"+dumpFile; + } + if (ionic.Platform.isIOS()) { + alert(i18next.t('email-service.email-account-mail-app')); + } + var email = { + attachments: [ + attachFile + ], + subject: i18next.t('email-service.email-data.subject-data-dump-from-to', {start: startMoment.format(fmt),end: endMoment.format(fmt)}), + body: i18next.t('email-service.email-data.body-data-consists-of-list-of-entries') + } + $window.cordova.plugins.email.open(email).then(resolve()); } - var email = { - attachments: [attachFile], - subject: i18next.t('email-service.email-data.subject-data-dump-from-to', { - start: startMoment.format(fmt), - end: endMoment.format(fmt), - }), - body: i18next.t( - 'email-service.email-data.body-data-consists-of-list-of-entries', - ), - }; - $window.cordova.plugins.email.open(email).then(resolve()); - }; - reader.readAsText(file); - }, - function (error) { - $ionicPopup.alert({ - title: 'Error while downloading JSON dump', - template: error, + reader.readAsText(file); + }, function(error) { + $ionicPopup.alert({title: "Error while downloading JSON dump", + template: error}); + reject(error); }); - reject(error); - }, - ); + }); + }); }); - }); - }); - }; + }; - CommHelper.getRawEntries(null, startMoment.unix(), endMoment.unix()) - .then(writeDumpFile) - .then(emailData) - .then(function () { - Logger.log('Email queued successfully'); - }) - .catch(function (error) { - Logger.displayError('Error emailing JSON dump', error); - }); + CommHelper.getRawEntries(null, startMoment.unix(), endMoment.unix()) + .then(writeDumpFile) + .then(emailData) + .then(function() { + Logger.log("Email queued successfully"); + }) + .catch(function(error) { + Logger.displayError("Error emailing JSON dump", error); + }) }; - this.getOPCode = function () { + this.getOPCode = function() { return window.cordova.plugins.OPCodeAuth.getOPCode(); }; - this.getSettings = function () { + 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]; - } + +}) + +.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; - }, - }; - }); + } + return null; + } + }; +}); diff --git a/www/js/splash/customURL.js b/www/js/splash/customURL.js index 3a2f51598..521244bc0 100644 --- a/www/js/splash/customURL.js +++ b/www/js/splash/customURL.js @@ -2,40 +2,39 @@ import angular from 'angular'; -angular - .module('emission.splash.customURLScheme', []) +angular.module('emission.splash.customURLScheme', []) - .factory('CustomURLScheme', function ($rootScope) { +.factory('CustomURLScheme', function($rootScope) { var cus = {}; - var parseURL = function (url) { - var addr = url.split('//')[1]; - var route = addr.split('?')[0]; - var params = addr.split('?')[1]; - var paramsList = params.split('&'); - var rtn = { route: route }; - for (var i = 0; i < paramsList.length; i++) { - var splitList = paramsList[i].split('='); - rtn[splitList[0]] = splitList[1]; - } - return rtn; + var parseURL = function(url) { + var addr = url.split('//')[1]; + var route = addr.split('?')[0]; + var params = addr.split('?')[1]; + var paramsList = params.split('&'); + var rtn = {route: route}; + for (var i = 0; i < paramsList.length; i++) { + var splitList = paramsList[i].split('='); + rtn[splitList[0]] = splitList[1]; + } + return rtn; }; /* * Register a custom URL handler. * handler arguments are: * - * event: + * event: * url: the url that was passed in * urlComponents: the URL parsed into multiple components */ - cus.onLaunch = function (handler) { - console.log('onLaunch method from factory called'); - $rootScope.$on('CUSTOM_URL_LAUNCH', function (event, url) { - var urlComponents = parseURL(url); - handler(event, url, urlComponents); - }); + cus.onLaunch = function(handler) { + console.log("onLaunch method from factory called"); + $rootScope.$on("CUSTOM_URL_LAUNCH", function(event, url) { + var urlComponents = parseURL(url); + handler(event, url, urlComponents); + }); }; return cus; - }); +}); diff --git a/www/js/splash/localnotify.js b/www/js/splash/localnotify.js index 74c1637ff..6a4241f2c 100644 --- a/www/js/splash/localnotify.js +++ b/www/js/splash/localnotify.js @@ -7,136 +7,103 @@ import angular from 'angular'; -angular - .module('emission.splash.localnotify', [ - 'emission.plugin.logger', - 'emission.splash.startprefs', - 'ionic-toast', - ]) - .factory( - 'LocalNotify', - function ($window, $ionicPlatform, $ionicPopup, $state, $rootScope, ionicToast, Logger) { - var localNotify = {}; +angular.module('emission.splash.localnotify', ['emission.plugin.logger', + 'emission.splash.startprefs', + 'ionic-toast']) +.factory('LocalNotify', function($window, $ionicPlatform, $ionicPopup, + $state, $rootScope, ionicToast, Logger) { + var localNotify = {}; - /* - * Return the state to redirect to, undefined otherwise - */ - localNotify.getRedirectState = function (data) { - // TODO: Think whether this should be in data or in category - if (angular.isDefined(data)) { - return [data.redirectTo, data.redirectParams]; - } - return undefined; - }; + /* + * Return the state to redirect to, undefined otherwise + */ + localNotify.getRedirectState = function(data) { + // TODO: Think whether this should be in data or in category + if (angular.isDefined(data)) { + return [data.redirectTo, data.redirectParams]; + } + return undefined; + } - localNotify.handleLaunch = function (targetState, targetParams) { - $rootScope.redirectTo = targetState; - $rootScope.redirectParams = targetParams; - $state.go(targetState, targetParams, { reload: true }); - }; + localNotify.handleLaunch = function(targetState, targetParams) { + $rootScope.redirectTo = targetState; + $rootScope.redirectParams = targetParams; + $state.go(targetState, targetParams, { reload : true }); + } - localNotify.handlePrompt = function (notification, targetState, targetParams) { - Logger.log( - 'Prompting for notification ' + notification.title + ' and text ' + notification.text, - ); - var promptPromise = $ionicPopup.show({ - title: notification.title, - template: notification.text, - buttons: [ - { - text: 'Handle', - type: 'button-positive', - onTap: function (e) { - // e.preventDefault() will stop the popup from closing when tapped. - return true; - }, - }, - { - text: 'Ignore', - type: 'button-positive', - onTap: function (e) { - return false; - }, - }, - ], - }); - promptPromise.then(function (handle) { - if (handle == true) { - localNotify.handleLaunch(targetState, targetParams); - } else { - Logger.log( - 'Ignoring notification ' + notification.title + ' and text ' + notification.text, - ); + localNotify.handlePrompt = function(notification, targetState, targetParams) { + Logger.log("Prompting for notification "+notification.title+" and text "+notification.text); + var promptPromise = $ionicPopup.show({title: notification.title, + template: notification.text, + buttons: [{ + text: 'Handle', + type: 'button-positive', + onTap: function(e) { + // e.preventDefault() will stop the popup from closing when tapped. + return true; } - }); - }; - - localNotify.handleNotification = function (notification, state, data) { - // Comment this out for ease of testing. But in the real world, we do in fact want to - // cancel the notification to avoid "hey! I just fixed this, why is the notification still around!" - // issues - // $window.cordova.plugins.notification.local.cancel(notification.id); - let redirectData = notification; - if (state.event == 'action') { - redirectData = notification.data.action; - } - var [targetState, targetParams] = localNotify.getRedirectState(redirectData); - Logger.log('targetState = ' + targetState); - if (angular.isDefined(targetState)) { - if (state.foreground == true) { - localNotify.handlePrompt(notification, targetState, targetParams); - } else { - localNotify.handleLaunch(targetState, targetParams); + }, { + text: 'Ignore', + type: 'button-positive', + onTap: function(e) { + return false; } - } - }; + }] + }); + promptPromise.then(function(handle) { + if (handle == true) { + localNotify.handleLaunch(targetState, targetParams); + } else { + Logger.log("Ignoring notification "+notification.title+" and text "+notification.text); + } + }); + } + + localNotify.handleNotification = function(notification,state,data) { + // Comment this out for ease of testing. But in the real world, we do in fact want to + // cancel the notification to avoid "hey! I just fixed this, why is the notification still around!" + // issues + // $window.cordova.plugins.notification.local.cancel(notification.id); + let redirectData = notification; + if (state.event == 'action') { + redirectData = notification.data.action; + } + var [targetState, targetParams] = localNotify.getRedirectState(redirectData); + Logger.log("targetState = "+targetState); + if (angular.isDefined(targetState)) { + if (state.foreground == true) { + localNotify.handlePrompt(notification, targetState, targetParams); + } else { + localNotify.handleLaunch(targetState, targetParams); + } + } + } - localNotify.registerRedirectHandler = function () { - Logger.log('registerUserResponse received!'); - $window.cordova.plugins.notification.local.on( - 'action', - function (notification, state, data) { - localNotify.handleNotification(notification, state, data); - }, - ); - $window.cordova.plugins.notification.local.on( - 'clear', - function (notification, state, data) { - // alert("notification cleared, no report"); - }, - ); - $window.cordova.plugins.notification.local.on( - 'cancel', - function (notification, state, data) { - // alert("notification cancelled, no report"); - }, - ); - $window.cordova.plugins.notification.local.on( - 'trigger', - function (notification, state, data) { - ionicToast.show( - `Notification: ${notification.title}\n${notification.text}`, - 'bottom', - false, - 250000, - ); - localNotify.handleNotification(notification, state, data); - }, - ); - $window.cordova.plugins.notification.local.on( - 'click', - function (notification, state, data) { - localNotify.handleNotification(notification, state, data); - }, - ); - }; + localNotify.registerRedirectHandler = function() { + Logger.log( "registerUserResponse received!" ); + $window.cordova.plugins.notification.local.on('action', function (notification, state, data) { + localNotify.handleNotification(notification, state, data); + }); + $window.cordova.plugins.notification.local.on('clear', function (notification, state, data) { + // alert("notification cleared, no report"); + }); + $window.cordova.plugins.notification.local.on('cancel', function (notification, state, data) { + // alert("notification cancelled, no report"); + }); + $window.cordova.plugins.notification.local.on('trigger', function (notification, state, data) { + ionicToast.show(`Notification: ${notification.title}\n${notification.text}`, 'bottom', false, 250000); + localNotify.handleNotification(notification, state, data); + }); + $window.cordova.plugins.notification.local.on('click', function (notification, state, data) { + localNotify.handleNotification(notification, state, data); + }); + } - $ionicPlatform.ready().then(function () { - localNotify.registerRedirectHandler(); - Logger.log('finished registering handlers, about to fire queued events'); - $window.cordova.plugins.notification.local.fireQueuedEvents(); - }); + $ionicPlatform.ready().then(function() { + localNotify.registerRedirectHandler(); + Logger.log("finished registering handlers, about to fire queued events"); + $window.cordova.plugins.notification.local.fireQueuedEvents(); + }); - return localNotify; - }, - ); + return localNotify; +}); diff --git a/www/js/splash/notifScheduler.js b/www/js/splash/notifScheduler.js index 6f909e436..069af7a18 100644 --- a/www/js/splash/notifScheduler.js +++ b/www/js/splash/notifScheduler.js @@ -2,269 +2,270 @@ import angular from 'angular'; -angular - .module('emission.splash.notifscheduler', [ - 'emission.services', - 'emission.plugin.logger', - 'emission.stats.clientstats', - 'emission.config.dynamic', - ]) +angular.module('emission.splash.notifscheduler', + ['emission.services', + 'emission.plugin.logger', + 'emission.stats.clientstats', + 'emission.config.dynamic']) - .factory( - 'NotificationScheduler', - function ($http, $window, $ionicPlatform, ClientStats, DynamicConfig, CommHelper, Logger) { - const scheduler = {}; - let _config; - let scheduledPromise = new Promise((rs) => rs()); - let isScheduling = false; +.factory('NotificationScheduler', function($http, $window, $ionicPlatform, + ClientStats, DynamicConfig, CommHelper, Logger) { - // like python range() - function range(start, stop, step) { - let a = [start], - b = start; - while (b < stop) a.push((b += step || 1)); + const scheduler = {}; + let _config; + let scheduledPromise = new Promise((rs) => rs()); + let isScheduling = false; + + // like python range() + function range(start, stop, step) { + let a = [start], b = start; + while (b < stop) + a.push(b += step || 1); return a; - } + } - // returns an array of moment objects, for all times that notifications should be sent - const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { + // returns an array of moment objects, for all times that notifications should be sent + const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { const notifTimes = []; for (const s of scheme.schedule) { - // the days to send notifications, as integers, relative to day zero - const notifDays = range(s.start, s.end, s.intervalInDays); - for (const d of notifDays) { - const date = moment(dayZeroDate).add(d, 'days').format('YYYY-MM-DD'); - const notifTime = moment(date + ' ' + timeOfDay, 'YYYY-MM-DD HH:mm'); - notifTimes.push(notifTime); - } + // the days to send notifications, as integers, relative to day zero + const notifDays = range(s.start, s.end, s.intervalInDays); + for (const d of notifDays) { + const date = moment(dayZeroDate).add(d, 'days').format('YYYY-MM-DD') + const notifTime = moment(date+' '+timeOfDay, 'YYYY-MM-DD HH:mm'); + notifTimes.push(notifTime); + } } return notifTimes; - }; + } - // returns true if all expected times are already scheduled - const areAlreadyScheduled = (notifs, expectedTimes) => { + // returns true if all expected times are already scheduled + const areAlreadyScheduled = (notifs, expectedTimes) => { for (const t of expectedTimes) { - if (!notifs.some((n) => moment(n.at).isSame(t))) { - return false; - } + if (!notifs.some((n) => moment(n.at).isSame(t))) { + return false; + } } return true; - }; + } - /* remove notif actions as they do not work, can restore post routing migration */ - // const setUpActions = () => { - // const action = { - // id: 'action', - // title: 'Change Time', - // launch: true - // }; - // return new Promise((rs) => { - // cordova.plugins.notification.local.addActions('reminder-actions', [action], rs); - // }); - // } + /* remove notif actions as they do not work, can restore post routing migration */ + // const setUpActions = () => { + // const action = { + // id: 'action', + // title: 'Change Time', + // launch: true + // }; + // return new Promise((rs) => { + // cordova.plugins.notification.local.addActions('reminder-actions', [action], rs); + // }); + // } - function debugGetScheduled(prefix) { + function debugGetScheduled(prefix) { cordova.plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length) return Logger.log(`${prefix}, there are no scheduled notifications`); - const time = moment(notifs?.[0].trigger.at).format('HH:mm'); - //was in plugin, changed to scheduler - scheduler.scheduledNotifs = notifs.map((n) => { - const time = moment(n.trigger.at).format('LT'); - const date = moment(n.trigger.at).format('LL'); - return { - key: date, - val: time, - }; - }); - //have the list of scheduled show up in this log - Logger.log( - `${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduler.scheduledNotifs[0].key} at ${scheduler.scheduledNotifs[0].val}`, - ); + if (!notifs?.length) + return Logger.log(`${prefix}, there are no scheduled notifications`); + const time = moment(notifs?.[0].trigger.at).format('HH:mm'); + //was in plugin, changed to scheduler + scheduler.scheduledNotifs = notifs.map((n) => { + const time = moment(n.trigger.at).format('LT'); + const date = moment(n.trigger.at).format('LL'); + return { + key: date, + val: time + } + }); + //have the list of scheduled show up in this log + Logger.log(`${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduler.scheduledNotifs[0].key} at ${scheduler.scheduledNotifs[0].val}`); }); - } + } - //new method to fetch notifications - scheduler.getScheduledNotifs = function () { + //new method to fetch notifications + scheduler.getScheduledNotifs = function() { return new Promise((resolve, reject) => { - /* if the notifications are still in active scheduling it causes problems + /* 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 + 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); + }) + } + }) + } - 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); - }); - }); - }; + //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) => { + // schedules the notifications using the cordova plugin + const scheduleNotifs = (scheme, notifTimes) => { return new Promise((rs) => { - isScheduling = true; - const localeCode = i18next.resolvedLanguage; - const nots = notifTimes.map((n) => { - const nDate = n.toDate(); - const seconds = nDate.getTime() / 1000; - return { - id: seconds, - title: scheme.title[localeCode], - text: scheme.text[localeCode], - trigger: { at: nDate }, - // actions: 'reminder-actions', - // data: { - // action: { - // redirectTo: 'root.main.control', - // redirectParams: { - // openTimeOfDayPicker: true - // } - // } - // } - }; - }); - cordova.plugins.notification.local.cancelAll(() => { - debugGetScheduled('After cancelling'); - cordova.plugins.notification.local.schedule(nots, () => { - debugGetScheduled('After scheduling'); - isScheduling = false; - rs(); //scheduling promise resolved here + isScheduling = true; + const localeCode = i18next.resolvedLanguage; + const nots = notifTimes.map((n) => { + const nDate = n.toDate(); + const seconds = nDate.getTime() / 1000; + return { + id: seconds, + title: scheme.title[localeCode], + text: scheme.text[localeCode], + trigger: {at: nDate}, + // actions: 'reminder-actions', + // data: { + // action: { + // redirectTo: 'root.main.control', + // redirectParams: { + // openTimeOfDayPicker: true + // } + // } + // } + } + }); + cordova.plugins.notification.local.cancelAll(() => { + debugGetScheduled("After cancelling"); + cordova.plugins.notification.local.schedule(nots, () => { + debugGetScheduled("After scheduling"); + isScheduling = false; + rs(); //scheduling promise resolved here + }); }); - }); }); - }; + } - // determines when notifications are needed, and schedules them if not already scheduled - const update = async () => { - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = - await scheduler.getReminderPrefs(); + // determines when notifications are needed, and schedules them if not already scheduled + const update = async () => { + const { reminder_assignment, + reminder_join_date, + reminder_time_of_day} = await scheduler.getReminderPrefs(); const scheme = _config.reminderSchemes[reminder_assignment]; const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); 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'); + cordova.plugins.notification.local.getScheduled((notifs) => { + if (areAlreadyScheduled(notifs, notifTimes)) { + Logger.log("Already scheduled, not scheduling again"); } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); - //enforcing end of scheduling to conisder update through - scheduledPromise.then(() => { - resolve(); - }); + // 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(); + }) + } + }); } - }); - } - }); + }); }); - }; + } - /* Randomly assign a scheme, set the join date to today, + /* Randomly assign a scheme, set the join date to today, and use the default time of day from config (or noon if not specified) This is only called once when the user first joins the study */ - const initReminderPrefs = () => { + const initReminderPrefs = () => { // randomly assign from the schemes listed in config const schemes = Object.keys(_config.reminderSchemes); const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; const todayDate = moment().format('YYYY-MM-DD'); const defaultTime = _config.reminderSchemes[randAssignment]?.defaultTime || '12:00'; return { - reminder_assignment: randAssignment, - reminder_join_date: todayDate, - reminder_time_of_day: defaultTime, + reminder_assignment: randAssignment, + reminder_join_date: todayDate, + reminder_time_of_day: defaultTime, }; - }; + } - /* EXAMPLE VALUES - present in user profile object + /* EXAMPLE VALUES - present in user profile object reminder_assignment: 'passive', reminder_join_date: '2023-05-09', reminder_time_of_day: '21:00', */ - scheduler.getReminderPrefs = async () => { + scheduler.getReminderPrefs = async () => { const user = await CommHelper.getUser(); - if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { - return user; + if (user?.reminder_assignment && + user?.reminder_join_date && + user?.reminder_time_of_day) { + return user; } // if no prefs, user just joined, so initialize them const initPrefs = initReminderPrefs(); await scheduler.setReminderPrefs(initPrefs); return { ...user, ...initPrefs }; // user profile + the new prefs - }; + } - scheduler.setReminderPrefs = async (newPrefs) => { - await CommHelper.updateUser(newPrefs); + scheduler.setReminderPrefs = async (newPrefs) => { + await CommHelper.updateUser(newPrefs) const updatePromise = new Promise((resolve, reject) => { - //enforcing update before moving on - update().then(() => { - resolve(); - }); + //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, - // and add as a reading to client stats - const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; - ClientStats.addReading(ClientStats.getStatKeys().REMINDER_PREFS, { - reminder_assignment, - reminder_join_date, - reminder_time_of_day, - }).then(Logger.log('Added reminder prefs to client stats')); + // extract only the relevant fields from the prefs, + // and add as a reading to client stats + const { reminder_assignment, + reminder_join_date, + reminder_time_of_day} = prefs; + ClientStats.addReading(ClientStats.getStatKeys().REMINDER_PREFS, { + reminder_assignment, + reminder_join_date, + reminder_time_of_day + }).then(Logger.log("Added reminder prefs to client stats")); }); return updatePromise; - }; + } - $ionicPlatform.ready().then(async () => { + $ionicPlatform.ready().then(async () => { _config = await DynamicConfig.configReady(); if (!_config.reminderSchemes) { - Logger.log('No reminder schemes found in config, not scheduling notifications'); - return; + Logger.log("No reminder schemes found in config, not scheduling notifications"); + return; } //setUpActions(); update(); - }); + }); - return scheduler; - }, - ); + return scheduler; +}); diff --git a/www/js/splash/pushnotify.js b/www/js/splash/pushnotify.js index e7881d46f..66c70f45c 100644 --- a/www/js/splash/pushnotify.js +++ b/www/js/splash/pushnotify.js @@ -15,207 +15,175 @@ import angular from 'angular'; -angular - .module('emission.splash.pushnotify', [ - 'emission.plugin.logger', - 'emission.services', - 'emission.splash.startprefs', - ]) - .factory( - 'PushNotify', - function ( - $window, - $state, - $rootScope, - $ionicPlatform, - $ionicPopup, - Logger, - CommHelper, - StartPrefs, - ) { - var pushnotify = {}; - var push = null; - pushnotify.CLOUD_NOTIFICATION_EVENT = 'cloud:push:notification'; +angular.module('emission.splash.pushnotify', ['emission.plugin.logger', + 'emission.services', + 'emission.splash.startprefs']) +.factory('PushNotify', function($window, $state, $rootScope, $ionicPlatform, + $ionicPopup, Logger, CommHelper, StartPrefs) { - pushnotify.startupInit = function () { - push = $window.PushNotification.init({ - ios: { - badge: true, - sound: true, - vibration: true, - clearBadge: true, - }, - android: { - iconColor: '#008acf', - icon: 'ic_mood_question', - clearNotifications: true, - }, - }); - push.on('notification', function (data) { - if ($ionicPlatform.is('ios')) { + var pushnotify = {}; + var push = null; + pushnotify.CLOUD_NOTIFICATION_EVENT = 'cloud:push:notification'; + + pushnotify.startupInit = function() { + push = $window.PushNotification.init({ + "ios": { + "badge": true, + "sound": true, + "vibration": true, + "clearBadge": true + }, + "android": { + "iconColor": "#008acf", + "icon": "ic_mood_question", + "clearNotifications": true + } + }); + push.on('notification', function(data) { + if ($ionicPlatform.is('ios')) { // Parse the iOS values that are returned as strings - if (angular.isDefined(data) && angular.isDefined(data.additionalData)) { - if (angular.isDefined(data.additionalData.payload)) { - data.additionalData.payload = JSON.parse(data.additionalData.payload); - } - if ( - angular.isDefined(data.additionalData.data) && - typeof data.additionalData.data == 'string' - ) { - data.additionalData.data = JSON.parse(data.additionalData.data); - } else { - console.log('additionalData is already an object, no need to parse it'); - } + if(angular.isDefined(data) && + angular.isDefined(data.additionalData)) { + if(angular.isDefined(data.additionalData.payload)) { + data.additionalData.payload = JSON.parse(data.additionalData.payload); + } + if(angular.isDefined(data.additionalData.data) && typeof(data.additionalData.data) == "string") { + data.additionalData.data = JSON.parse(data.additionalData.data); + } else { + console.log("additionalData is already an object, no need to parse it"); + } } else { - Logger.log('No additional data defined, nothing to parse'); + Logger.log("No additional data defined, nothing to parse"); } - } - $rootScope.$emit(pushnotify.CLOUD_NOTIFICATION_EVENT, data); - }); - }; + } + $rootScope.$emit(pushnotify.CLOUD_NOTIFICATION_EVENT, data); + }); + } - pushnotify.registerPromise = function () { - return new Promise(function (resolve, reject) { - pushnotify.startupInit(); - push.on('registration', function (data) { - console.log('Got registration ' + data); - resolve({ token: data.registrationId, type: data.registrationType }); - }); - push.on('error', function (error) { - console.log('Got push error ' + error); - reject(error); - }); - console.log('push notify = ' + push); + pushnotify.registerPromise = function() { + return new Promise(function(resolve, reject) { + pushnotify.startupInit(); + push.on("registration", function(data) { + console.log("Got registration " + data); + resolve({token: data.registrationId, + type: data.registrationType}); + }); + push.on("error", function(error) { + console.log("Got push error " + error); + reject(error); + }); + console.log("push notify = "+push); }); - }; + } - pushnotify.registerPush = function () { - pushnotify - .registerPromise() - .then(function (t) { - // alert("Token = "+JSON.stringify(t)); - Logger.log('Token = ' + JSON.stringify(t)); - return $window.cordova.plugins.BEMServerSync.getConfig() - .then( - function (config) { - return config.sync_interval; - }, - function (error) { - console.log( - 'Got error ' + error + ' while reading config, returning default = 3600', - ); - return 3600; - }, - ) - .then(function (sync_interval) { - CommHelper.updateUser({ - device_token: t.token, - curr_platform: ionic.Platform.platform(), - curr_sync_interval: sync_interval, - }); - return t; - }); - }) - .then(function (t) { - // alert("Finished saving token = "+JSON.stringify(t.token)); - Logger.log('Finished saving token = ' + JSON.stringify(t.token)); - }) - .catch(function (error) { - Logger.displayError('Error in registering push notifications', error); - }); - }; + pushnotify.registerPush = function() { + pushnotify.registerPromise().then(function(t) { + // alert("Token = "+JSON.stringify(t)); + Logger.log("Token = "+JSON.stringify(t)); + return $window.cordova.plugins.BEMServerSync.getConfig().then(function(config) { + return config.sync_interval; + }, function(error) { + console.log("Got error "+error+" while reading config, returning default = 3600"); + return 3600; + }).then(function(sync_interval) { + CommHelper.updateUser({ + device_token: t.token, + curr_platform: ionic.Platform.platform(), + curr_sync_interval: sync_interval + }); + return t; + }); + }).then(function(t) { + // alert("Finished saving token = "+JSON.stringify(t.token)); + Logger.log("Finished saving token = "+JSON.stringify(t.token)); + }).catch(function(error) { + Logger.displayError("Error in registering push notifications", error); + }); + } - var redirectSilentPush = function (event, data) { - Logger.log('Found silent push notification, for platform ' + ionic.Platform.platform()); + var redirectSilentPush = function(event, data) { + Logger.log("Found silent push notification, for platform "+ionic.Platform.platform()); if (!$ionicPlatform.is('ios')) { - Logger.log('Platform is not ios, handleSilentPush is not implemented or needed'); + Logger.log("Platform is not ios, handleSilentPush is not implemented or needed"); // doesn't matter if we finish or not because platforms other than ios don't care return; } - Logger.log('Platform is ios, calling handleSilentPush on DataCollection'); + Logger.log("Platform is ios, calling handleSilentPush on DataCollection"); var notId = data.additionalData.payload.notId; - var finishErrFn = function (error) { - Logger.log('in push.finish, error = ' + error); + var finishErrFn = function(error) { + Logger.log("in push.finish, error = "+error); }; - pushnotify.datacollect - .getConfig() - .then(function (config) { - if (config.ios_use_remote_push_for_sync) { - pushnotify.datacollect.handleSilentPush().then(function () { - Logger.log('silent push finished successfully, calling push.finish'); - showDebugLocalNotification('silent push finished, calling push.finish'); - push.finish(function () {}, finishErrFn, notId); + pushnotify.datacollect.getConfig().then(function(config) { + if(config.ios_use_remote_push_for_sync) { + pushnotify.datacollect.handleSilentPush() + .then(function() { + Logger.log("silent push finished successfully, calling push.finish"); + showDebugLocalNotification("silent push finished, calling push.finish"); + push.finish(function(){}, finishErrFn, notId); + }) + } else { + Logger.log("Using background fetch for sync, no need to redirect push"); + push.finish(function(){}, finishErrFn, notId); + }; + }) + .catch(function(error) { + push.finish(function(){}, finishErrFn, notId); + Logger.displayError("Error while redirecting silent push", error); + }); + } + + var showDebugLocalNotification = function(message) { + pushnotify.datacollect.getConfig().then(function(config) { + if(config.simulate_user_interaction) { + cordova.plugins.notification.local.schedule({ + id: 1, + title: "Debug javascript notification", + text: message, + actions: [], + category: 'SIGN_IN_TO_CLASS' }); - } else { - Logger.log('Using background fetch for sync, no need to redirect push'); - push.finish(function () {}, finishErrFn, notId); } - }) - .catch(function (error) { - push.finish(function () {}, finishErrFn, notId); - Logger.displayError('Error while redirecting silent push', error); - }); - }; - - var showDebugLocalNotification = function (message) { - pushnotify.datacollect.getConfig().then(function (config) { - if (config.simulate_user_interaction) { - cordova.plugins.notification.local.schedule({ - id: 1, - title: 'Debug javascript notification', - text: message, - actions: [], - category: 'SIGN_IN_TO_CLASS', - }); - } }); - }; + } - pushnotify.registerNotificationHandler = function () { - $rootScope.$on(pushnotify.CLOUD_NOTIFICATION_EVENT, function (event, data) { - Logger.log('data = ' + JSON.stringify(data)); - if (data.additionalData['content-available'] == 1) { - redirectSilentPush(event, data); - } // else no need to call finish - }); - }; + pushnotify.registerNotificationHandler = function() { + $rootScope.$on(pushnotify.CLOUD_NOTIFICATION_EVENT, function(event, data) { + Logger.log("data = "+JSON.stringify(data)); + if (data.additionalData["content-available"] == 1) { + redirectSilentPush(event, data); + }; // else no need to call finish + }); + }; - $ionicPlatform.ready().then(function () { - pushnotify.datacollect = $window.cordova.plugins.BEMDataCollection; - StartPrefs.readConsentState() - .then(StartPrefs.isConsented) - .then(function (consentState) { - if (consentState == true) { + $ionicPlatform.ready().then(function() { + pushnotify.datacollect = $window.cordova.plugins.BEMDataCollection; + StartPrefs.readConsentState() + .then(StartPrefs.isConsented) + .then(function(consentState) { + if (consentState == true) { pushnotify.registerPush(); - } else { - Logger.log('no consent yet, waiting to sign up for remote push'); - } - }); - pushnotify.registerNotificationHandler(); - Logger.log('pushnotify startup done'); - }); + } else { + Logger.log("no consent yet, waiting to sign up for remote push"); + } + }); + pushnotify.registerNotificationHandler(); + Logger.log("pushnotify startup done"); + }); - $rootScope.$on(StartPrefs.CONSENTED_EVENT, function (event, data) { - console.log( - 'got consented event ' + - JSON.stringify(event.name) + - ' with data ' + - JSON.stringify(data), - ); - if (StartPrefs.isIntroDone()) { - console.log('intro is done -> reconsent situation, we already have a token -> register'); + $rootScope.$on(StartPrefs.CONSENTED_EVENT, function(event, data) { + console.log("got consented event "+JSON.stringify(event.name) + +" with data "+ JSON.stringify(data)); + if (StartPrefs.isIntroDone()) { + console.log("intro is done -> reconsent situation, we already have a token -> register"); pushnotify.registerPush(); - } - }); + } + }); - $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function (event, data) { - console.log( - 'intro is done -> original consent situation, we should have a token by now -> register', - ); - pushnotify.registerPush(); - }); + $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function(event, data) { + console.log("intro is done -> original consent situation, we should have a token by now -> register"); + pushnotify.registerPush(); + }); - return pushnotify; - }, - ); + return pushnotify; +}); diff --git a/www/js/splash/referral.js b/www/js/splash/referral.js index 175ce83df..9e4707200 100644 --- a/www/js/splash/referral.js +++ b/www/js/splash/referral.js @@ -1,9 +1,8 @@ import angular from 'angular'; -angular - .module('emission.splash.referral', ['emission.plugin.kvstore']) +angular.module('emission.splash.referral', ['emission.plugin.kvstore']) - .factory('ReferralHandler', function ($window, KVStore) { +.factory('ReferralHandler', function($window, KVStore) { var referralHandler = {}; var REFERRAL_NAVIGATION_KEY = 'referral_navigation'; @@ -11,33 +10,34 @@ angular var REFERRED_GROUP_ID = 'referred_group_id'; var REFERRED_USER_ID = 'referred_user_id'; - referralHandler.getReferralNavigation = function () { + referralHandler.getReferralNavigation = function() { const toReturn = KVStore.getDirect(REFERRAL_NAVIGATION_KEY); KVStore.remove(REFERRAL_NAVIGATION_KEY); return toReturn; - }; - - referralHandler.setupGroupReferral = function (kvList) { - KVStore.set(REFERRED_KEY, true); - KVStore.set(REFERRED_GROUP_ID, kvList['groupid']); - KVStore.set(REFERRED_USER_ID, kvList['userid']); - KVStore.set(REFERRAL_NAVIGATION_KEY, 'goals'); - }; - - referralHandler.clearGroupReferral = function (kvList) { - KVStore.remove(REFERRED_KEY); - KVStore.remove(REFERRED_GROUP_ID); - KVStore.remove(REFERRED_USER_ID); - KVStore.remove(REFERRAL_NAVIGATION_KEY); - }; - - referralHandler.getReferralParams = function (kvList) { - return [KVStore.getDirect(REFERRED_GROUP_ID), KVStore.getDirect(REFERRED_USER_ID)]; - }; - - referralHandler.hasPendingRegistration = function () { - return KVStore.getDirect(REFERRED_KEY); - }; - - return referralHandler; - }); + } + + referralHandler.setupGroupReferral = function(kvList) { + KVStore.set(REFERRED_KEY, true); + KVStore.set(REFERRED_GROUP_ID, kvList['groupid']); + KVStore.set(REFERRED_USER_ID, kvList['userid']); + KVStore.set(REFERRAL_NAVIGATION_KEY, 'goals'); + }; + + referralHandler.clearGroupReferral = function(kvList) { + KVStore.remove(REFERRED_KEY); + KVStore.remove(REFERRED_GROUP_ID); + KVStore.remove(REFERRED_USER_ID); + KVStore.remove(REFERRAL_NAVIGATION_KEY); + }; + + referralHandler.getReferralParams = function(kvList) { + return [KVStore.getDirect(REFERRED_GROUP_ID), + KVStore.getDirect(REFERRED_USER_ID)]; + } + + referralHandler.hasPendingRegistration = function() { + return KVStore.getDirect(REFERRED_KEY) + }; + + return referralHandler; +}); diff --git a/www/js/splash/remotenotify.js b/www/js/splash/remotenotify.js index 7f57610fd..2074da5b8 100644 --- a/www/js/splash/remotenotify.js +++ b/www/js/splash/remotenotify.js @@ -14,84 +14,66 @@ import angular from 'angular'; -angular - .module('emission.splash.remotenotify', [ - 'emission.plugin.logger', - 'emission.splash.startprefs', - 'emission.stats.clientstats', - ]) +angular.module('emission.splash.remotenotify', ['emission.plugin.logger', + 'emission.splash.startprefs', + 'emission.stats.clientstats']) - .factory( - 'RemoteNotify', - function ($http, $window, $ionicPopup, $rootScope, ClientStats, CommHelper, Logger) { - var remoteNotify = {}; - remoteNotify.options = 'location=yes,clearcache=no,toolbar=yes,hideurlbar=yes'; +.factory('RemoteNotify', function($http, $window, $ionicPopup, $rootScope, ClientStats, + CommHelper, Logger) { - /* + var remoteNotify = {}; + remoteNotify.options = "location=yes,clearcache=no,toolbar=yes,hideurlbar=yes"; + + /* TODO: Potentially unify with the survey URL loading */ - remoteNotify.launchWebpage = function (url) { - // THIS LINE FOR inAppBrowser - let iab = $window.cordova.InAppBrowser.open(url, '_blank', remoteNotify.options); - }; + remoteNotify.launchWebpage = function(url) { + // THIS LINE FOR inAppBrowser + let iab = $window.cordova.InAppBrowser.open(url, '_blank', remoteNotify.options); + } - remoteNotify.launchPopup = function (title, text) { - // THIS LINE FOR inAppBrowser - let alertPopup = $ionicPopup.alert({ - title: title, - template: text, - }); - }; + remoteNotify.launchPopup = function(title, text) { + // THIS LINE FOR inAppBrowser + let alertPopup = $ionicPopup.alert({ + title: title, + template: text + }); + } - remoteNotify.init = function () { - $rootScope.$on('cloud:push:notification', function (event, data) { - ClientStats.addEvent(ClientStats.getStatKeys().NOTIFICATION_OPEN).then(function () { - console.log( - 'Added ' + - ClientStats.getStatKeys().NOTIFICATION_OPEN + - ' event. Data = ' + - JSON.stringify(data), - ); - }); - Logger.log('data = ' + JSON.stringify(data)); - if ( - angular.isDefined(data.additionalData) && + remoteNotify.init = function() { + $rootScope.$on('cloud:push:notification', function(event, data) { + ClientStats.addEvent(ClientStats.getStatKeys().NOTIFICATION_OPEN).then( + function() { + console.log("Added "+ClientStats.getStatKeys().NOTIFICATION_OPEN+" event. Data = " + JSON.stringify(data)); + }); + Logger.log("data = "+JSON.stringify(data)); + if (angular.isDefined(data.additionalData) && angular.isDefined(data.additionalData.payload) && - angular.isDefined(data.additionalData.payload.alert_type) - ) { - if (data.additionalData.payload.alert_type == 'website') { - var webpage_spec = data.additionalData.payload.spec; - if ( - angular.isDefined(webpage_spec) && - angular.isDefined(webpage_spec.url) && - webpage_spec.url.startsWith('https://') - ) { - remoteNotify.launchWebpage(webpage_spec.url); - } else { - $ionicPopup.alert( - 'webpage was not specified correctly. spec is ' + JSON.stringify(webpage_spec), - ); - } + angular.isDefined(data.additionalData.payload.alert_type)) { + if(data.additionalData.payload.alert_type == "website") { + var webpage_spec = data.additionalData.payload.spec; + if (angular.isDefined(webpage_spec) && + angular.isDefined(webpage_spec.url) && + webpage_spec.url.startsWith("https://")) { + remoteNotify.launchWebpage(webpage_spec.url); + } else { + $ionicPopup.alert("webpage was not specified correctly. spec is "+JSON.stringify(webpage_spec)); + } } - if (data.additionalData.payload.alert_type == 'popup') { - var popup_spec = data.additionalData.payload.spec; - if ( - angular.isDefined(popup_spec) && - angular.isDefined(popup_spec.title) && - angular.isDefined(popup_spec.text) - ) { - remoteNotify.launchPopup(popup_spec.title, popup_spec.text); - } else { - $ionicPopup.alert( - 'webpage was not specified correctly. spec is ' + JSON.stringify(popup_spec), - ); - } + if(data.additionalData.payload.alert_type == "popup") { + var popup_spec = data.additionalData.payload.spec; + if (angular.isDefined(popup_spec) && + angular.isDefined(popup_spec.title) && + angular.isDefined(popup_spec.text)) { + remoteNotify.launchPopup(popup_spec.title, popup_spec.text); + } else { + $ionicPopup.alert("webpage was not specified correctly. spec is "+JSON.stringify(popup_spec)); + } } - } - }); - }; + } + }); + } - remoteNotify.init(); - return remoteNotify; - }, - ); + remoteNotify.init(); + return remoteNotify; +}); diff --git a/www/js/splash/startprefs.js b/www/js/splash/startprefs.js index 7cb4b0e88..223c82579 100644 --- a/www/js/splash/startprefs.js +++ b/www/js/splash/startprefs.js @@ -1,316 +1,255 @@ import angular from 'angular'; -angular - .module('emission.splash.startprefs', [ - 'emission.plugin.logger', - 'emission.splash.referral', - 'emission.plugin.kvstore', - 'emission.config.dynamic', - ]) - - .factory( - 'StartPrefs', - function ( - $window, - $state, - $interval, - $rootScope, - $ionicPlatform, - $ionicPopup, - KVStore, - $http, - Logger, - ReferralHandler, - DynamicConfig, - ) { - var logger = Logger; - var nTimesCalled = 0; - var startprefs = {}; - // Boolean: represents that the "intro" - the one page summary - // and the login are done - var INTRO_DONE_KEY = 'intro_done'; - // data collection consented protocol: string, represents the date on - // which the consented protocol was approved by the IRB - var DATA_COLLECTION_CONSENTED_PROTOCOL = 'data_collection_consented_protocol'; - - var CONSENTED_KEY = 'config/consent'; - - startprefs.CONSENTED_EVENT = 'data_collection_consented'; - startprefs.INTRO_DONE_EVENT = 'intro_done'; - - var writeConsentToNative = function () { - return $window.cordova.plugins.BEMDataCollection.markConsented($rootScope.req_consent); - }; - - startprefs.markConsented = function () { - logger.log( - 'changing consent from ' + - $rootScope.curr_consented + - ' -> ' + - JSON.stringify($rootScope.req_consent), - ); - // mark in native storage - return startprefs - .readConsentState() - .then(writeConsentToNative) - .then(function (response) { - // mark in local storage - KVStore.set(DATA_COLLECTION_CONSENTED_PROTOCOL, $rootScope.req_consent); - // mark in local variable as well - $rootScope.curr_consented = angular.copy($rootScope.req_consent); - $rootScope.$emit(startprefs.CONSENTED_EVENT, $rootScope.req_consent); - }); - }; - - startprefs.markIntroDone = function () { - var currTime = moment().format(); - KVStore.set(INTRO_DONE_KEY, currTime); - $rootScope.$emit(startprefs.INTRO_DONE_EVENT, currTime); - }; - - // returns boolean - startprefs.readIntroDone = function () { - return KVStore.get(INTRO_DONE_KEY).then(function (read_val) { - logger.log('in readIntroDone, read_val = ' + JSON.stringify(read_val)); +angular.module('emission.splash.startprefs', ['emission.plugin.logger', + 'emission.splash.referral', + 'emission.plugin.kvstore', + 'emission.config.dynamic']) + +.factory('StartPrefs', function($window, $state, $interval, $rootScope, $ionicPlatform, + $ionicPopup, KVStore, $http, Logger, ReferralHandler, DynamicConfig) { + var logger = Logger; + var nTimesCalled = 0; + var startprefs = {}; + // Boolean: represents that the "intro" - the one page summary + // and the login are done + var INTRO_DONE_KEY = 'intro_done'; + // data collection consented protocol: string, represents the date on + // which the consented protocol was approved by the IRB + var DATA_COLLECTION_CONSENTED_PROTOCOL = 'data_collection_consented_protocol'; + + var CONSENTED_KEY = "config/consent"; + + startprefs.CONSENTED_EVENT = "data_collection_consented"; + startprefs.INTRO_DONE_EVENT = "intro_done"; + + var writeConsentToNative = function() { + return $window.cordova.plugins.BEMDataCollection.markConsented($rootScope.req_consent); + }; + + startprefs.markConsented = function() { + logger.log("changing consent from "+ + $rootScope.curr_consented+" -> "+JSON.stringify($rootScope.req_consent)); + // mark in native storage + return startprefs.readConsentState().then(writeConsentToNative).then(function(response) { + // mark in local storage + KVStore.set(DATA_COLLECTION_CONSENTED_PROTOCOL, + $rootScope.req_consent); + // mark in local variable as well + $rootScope.curr_consented = angular.copy($rootScope.req_consent); + $rootScope.$emit(startprefs.CONSENTED_EVENT, $rootScope.req_consent); + }); + }; + + startprefs.markIntroDone = function() { + var currTime = moment().format(); + KVStore.set(INTRO_DONE_KEY, currTime); + $rootScope.$emit(startprefs.INTRO_DONE_EVENT, currTime); + } + + // returns boolean + startprefs.readIntroDone = function() { + return KVStore.get(INTRO_DONE_KEY).then(function(read_val) { + logger.log("in readIntroDone, read_val = "+JSON.stringify(read_val)); $rootScope.intro_done = read_val; - }); - }; - - startprefs.isIntroDone = function () { - if ($rootScope.intro_done == null || $rootScope.intro_done == '') { - logger.log('in isIntroDone, returning false'); - $rootScope.is_intro_done = false; - return false; - } else { - logger.log('in isIntroDone, returning true'); - $rootScope.is_intro_done = true; - return true; - } - }; - - startprefs.isConsented = function () { - if ( - $rootScope.curr_consented == null || - $rootScope.curr_consented == '' || - $rootScope.curr_consented.approval_date != $rootScope.req_consent.approval_date - ) { - console.log('Not consented in local storage, need to show consent'); - $rootScope.is_consented = false; - return false; - } else { - console.log('Consented in local storage, no need to show consent'); - $rootScope.is_consented = true; - return true; - } - }; - - startprefs.readConsentState = function () { - // read consent state from the file and populate it - return $http - .get('json/startupConfig.json') - .then(function (startupConfigResult) { - $rootScope.req_consent = startupConfigResult.data.emSensorDataCollectionProtocol; - logger.log('required consent version = ' + JSON.stringify($rootScope.req_consent)); - return KVStore.get(DATA_COLLECTION_CONSENTED_PROTOCOL); - }) - .then(function (kv_store_consent) { - $rootScope.curr_consented = kv_store_consent; - console.assert( - angular.isDefined($rootScope.req_consent), - 'in readConsentState $rootScope.req_consent', - JSON.stringify($rootScope.req_consent), - ); - // we can just launch this, we don't need to wait for it - startprefs.checkNativeConsent(); + }); + } + + startprefs.isIntroDone = function() { + if ($rootScope.intro_done == null || $rootScope.intro_done == "") { + logger.log("in isIntroDone, returning false"); + $rootScope.is_intro_done = false; + return false; + } else { + logger.log("in isIntroDone, returning true"); + $rootScope.is_intro_done = true; + return true; + } + } + + startprefs.isConsented = function() { + if ($rootScope.curr_consented == null || $rootScope.curr_consented == "" || + $rootScope.curr_consented.approval_date != $rootScope.req_consent.approval_date) { + console.log("Not consented in local storage, need to show consent"); + $rootScope.is_consented = false; + return false; + } else { + console.log("Consented in local storage, no need to show consent"); + $rootScope.is_consented = true; + return true; + } + } + + startprefs.readConsentState = function() { + // read consent state from the file and populate it + return $http.get("json/startupConfig.json") + .then(function(startupConfigResult) { + $rootScope.req_consent = startupConfigResult.data.emSensorDataCollectionProtocol; + logger.log("required consent version = " + JSON.stringify($rootScope.req_consent)); + return KVStore.get(DATA_COLLECTION_CONSENTED_PROTOCOL); + }).then(function(kv_store_consent) { + $rootScope.curr_consented = kv_store_consent; + console.assert(angular.isDefined($rootScope.req_consent), "in readConsentState $rootScope.req_consent", JSON.stringify($rootScope.req_consent)); + // we can just launch this, we don't need to wait for it + startprefs.checkNativeConsent(); }); - }; + } - startprefs.readConfig = function () { - return DynamicConfig.loadSavedConfig().then( - (savedConfig) => ($rootScope.app_ui_label = savedConfig), - ); - }; + startprefs.readConfig = function() { + return DynamicConfig.loadSavedConfig().then((savedConfig) => $rootScope.app_ui_label = savedConfig); + } - startprefs.hasConfig = function () { - if ( - $rootScope.app_ui_label == undefined || + startprefs.hasConfig = function() { + if ($rootScope.app_ui_label == undefined || $rootScope.app_ui_label == null || - $rootScope.app_ui_label == '' - ) { - logger.log('Config not downloaded, need to show join screen'); - $rootScope.has_config = false; - return false; - } else { - $rootScope.has_config = true; - logger.log('Config downloaded, skipping join screen'); - return true; - } - }; - - /* - * getNextState() returns a promise, since reading the startupConfig is - * async. The promise returns an onboarding state to navigate to, or - * null for the default state - */ - - startprefs.getPendingOnboardingState = function () { - return startprefs.readStartupState().then(function ([ - is_intro_done, - is_consented, - has_config, - ]) { - if (!has_config) { - console.assert( - !$rootScope.has_config, - 'in getPendingOnboardingState first check, $rootScope.has_config', - JSON.stringify($rootScope.has_config), - ); + $rootScope.app_ui_label == "") { + logger.log("Config not downloaded, need to show join screen"); + $rootScope.has_config = false; + return false; + } else { + $rootScope.has_config = true; + logger.log("Config downloaded, skipping join screen"); + return true; + } + } + + /* + * getNextState() returns a promise, since reading the startupConfig is + * async. The promise returns an onboarding state to navigate to, or + * null for the default state + */ + + startprefs.getPendingOnboardingState = function() { + return startprefs.readStartupState().then(function([is_intro_done, is_consented, has_config]) { + if (!has_config) { + console.assert(!$rootScope.has_config, "in getPendingOnboardingState first check, $rootScope.has_config", JSON.stringify($rootScope.has_config)); return 'root.join'; - } else if (!is_intro_done) { - console.assert( - !$rootScope.intro_done, - 'in getPendingOnboardingState second check, $rootScope.intro_done', - JSON.stringify($rootScope.intro_done), - ); + } else if (!is_intro_done) { + console.assert(!$rootScope.intro_done, "in getPendingOnboardingState second check, $rootScope.intro_done", JSON.stringify($rootScope.intro_done)); return 'root.intro'; - } else { - // intro is done. Now let's check consent - console.assert( - is_intro_done, - 'in getPendingOnboardingState, local is_intro_done', - is_intro_done, - ); - console.assert( - $rootScope.is_intro_done, - 'in getPendingOnboardingState, $rootScope.intro_done', - $rootScope.intro_done, - ); + } else { + // intro is done. Now let's check consent + console.assert(is_intro_done, "in getPendingOnboardingState, local is_intro_done", is_intro_done); + console.assert($rootScope.is_intro_done, "in getPendingOnboardingState, $rootScope.intro_done", $rootScope.intro_done); if (is_consented) { - return null; + return null; } else { - return 'root.reconsent'; + return 'root.reconsent'; } - } - }); - }; - - /* - * Read the intro_done and consent_done variables into the $rootScope so that - * we can use them without making multiple native calls - */ - startprefs.readStartupState = function () { - console.log('STARTPREFS: about to read startup state'); - var readIntroPromise = startprefs.readIntroDone().then(startprefs.isIntroDone); - var readConsentPromise = startprefs.readConsentState().then(startprefs.isConsented); - var readConfigPromise = startprefs.readConfig().then(startprefs.hasConfig); + } + }); + }; + + /* + * Read the intro_done and consent_done variables into the $rootScope so that + * we can use them without making multiple native calls + */ + startprefs.readStartupState = function() { + console.log("STARTPREFS: about to read startup state"); + var readIntroPromise = startprefs.readIntroDone() + .then(startprefs.isIntroDone); + var readConsentPromise = startprefs.readConsentState() + .then(startprefs.isConsented); + var readConfigPromise = startprefs.readConfig() + .then(startprefs.hasConfig); return Promise.all([readIntroPromise, readConsentPromise, readConfigPromise]); - }; - - startprefs.getConsentDocument = function () { - return $window.cordova.plugins.BEMUserCache.getDocument('config/consent', false).then( - function (resultDoc) { - if ($window.cordova.plugins.BEMUserCache.isEmptyDoc(resultDoc)) { - return null; - } else { - return resultDoc; + }; + + startprefs.getConsentDocument = function() { + return $window.cordova.plugins.BEMUserCache.getDocument("config/consent", false) + .then(function(resultDoc) { + if ($window.cordova.plugins.BEMUserCache.isEmptyDoc(resultDoc)) { + return null; + } else { + return resultDoc; + } + }); + }; + + startprefs.checkNativeConsent = function() { + startprefs.getConsentDocument().then(function(resultDoc) { + if (resultDoc == null) { + if(startprefs.isConsented()) { + logger.log("Local consent found, native consent missing, writing consent to native"); + $ionicPopup.alert({template: "Local consent found, native consent missing, writing consent to native"}); + return writeConsentToNative(); + } else { + logger.log("Both local and native consent not found, nothing to sync"); + } } - }, - ); - }; - - startprefs.checkNativeConsent = function () { - startprefs.getConsentDocument().then(function (resultDoc) { - if (resultDoc == null) { - if (startprefs.isConsented()) { - logger.log('Local consent found, native consent missing, writing consent to native'); - $ionicPopup.alert({ - template: 'Local consent found, native consent missing, writing consent to native', - }); - return writeConsentToNative(); - } else { - logger.log('Both local and native consent not found, nothing to sync'); - } - } }); - }; - - startprefs.getNextState = function () { - return startprefs - .getPendingOnboardingState() - .then(function (result) { - if (result == null) { - if (angular.isDefined($rootScope.redirectTo)) { - var redirState = $rootScope.redirectTo; - var redirParams = $rootScope.redirectParams; - $rootScope.redirectTo = undefined; - $rootScope.redirectParams = undefined; - return { state: redirState, params: redirParams }; - } else { - return { state: 'root.main.inf_scroll', params: {} }; - } - } else { - return { state: result, params: {} }; - } - }) - .catch((err) => { - Logger.displayError('error getting next state', err); - return 'root.intro'; - }); - }; + } + + startprefs.getNextState = function() { + return startprefs.getPendingOnboardingState().then(function(result){ + if (result == null) { + if (angular.isDefined($rootScope.redirectTo)) { + var redirState = $rootScope.redirectTo; + var redirParams = $rootScope.redirectParams; + $rootScope.redirectTo = undefined; + $rootScope.redirectParams = undefined; + return {state: redirState, params: redirParams}; + } else { + return {state: 'root.main.inf_scroll', params: {}}; + } + } else { + return {state: result, params: {}}; + } + }) + .catch((err) => { + Logger.displayError("error getting next state", err); + return "root.intro"; + }); + }; - var changeState = function (destState) { - logger.log('changing state to ' + destState); - console.log('loading ' + destState); + var changeState = function(destState) { + logger.log('changing state to '+destState); + console.log("loading "+destState); // TODO: Fix this the right way when we fix the FSM // https://github.com/e-mission/e-mission-phone/issues/146#issuecomment-251061736 var reload = false; - if ($state.$current == destState.state && $state.$current.name == 'root.main.goals') { + if (($state.$current == destState.state) && ($state.$current.name == 'root.main.goals')) { reload = true; } - $state.go(destState.state, destState.params).then(function () { - if (reload) { - $rootScope.$broadcast('RELOAD_GOAL_PAGE_FOR_REFERRAL'); - } + $state.go(destState.state, destState.params).then(function() { + if (reload) { + $rootScope.$broadcast("RELOAD_GOAL_PAGE_FOR_REFERRAL") + } }); - }; - - // Currently loads main or intro based on whether onboarding is complete. - // But easily extensible to storing the last screen that the user was on, - // or the users' preferred screen - - startprefs.loadPreferredScreen = function () { - logger.log('About to navigate to preferred tab'); - startprefs - .getNextState() - .then(changeState) - .catch(function (error) { - logger.displayError('Error loading preferred tab, loading root.intro', error); - // logger.log("error "+error+" loading finding tab, loading root.intro"); - changeState('root.intro'); - }); - }; - - startprefs.loadWithPrefs = function () { - // alert("attach debugger!"); - console.log('Checking to see whether we are ready to load the screen'); - if (!angular.isDefined($window.Logger)) { - alert('ionic is ready, but logger not present?'); - } - logger = Logger; - startprefs.loadPreferredScreen(); - }; - - startprefs.startWithPrefs = function () { - startprefs.loadWithPrefs(); - }; - - $ionicPlatform.ready().then(function () { - Logger.log('ionicPlatform.ready() called ' + nTimesCalled + ' times!'); - nTimesCalled = nTimesCalled + 1; - startprefs.startWithPrefs(); - Logger.log('startprefs startup done'); + }; + + // Currently loads main or intro based on whether onboarding is complete. + // But easily extensible to storing the last screen that the user was on, + // or the users' preferred screen + + startprefs.loadPreferredScreen = function() { + logger.log("About to navigate to preferred tab"); + startprefs.getNextState().then(changeState).catch(function(error) { + logger.displayError("Error loading preferred tab, loading root.intro", error); + // logger.log("error "+error+" loading finding tab, loading root.intro"); + changeState('root.intro'); }); - - return startprefs; - }, - ); + }; + + startprefs.loadWithPrefs = function() { + // alert("attach debugger!"); + console.log("Checking to see whether we are ready to load the screen"); + if (!angular.isDefined($window.Logger)) { + alert("ionic is ready, but logger not present?"); + } + logger = Logger; + startprefs.loadPreferredScreen(); + }; + + startprefs.startWithPrefs = function() { + startprefs.loadWithPrefs(); + } + + $ionicPlatform.ready().then(function() { + Logger.log("ionicPlatform.ready() called " + nTimesCalled+" times!"); + nTimesCalled = nTimesCalled + 1; + startprefs.startWithPrefs(); + Logger.log("startprefs startup done"); + }); + + return startprefs; +}); diff --git a/www/js/splash/storedevicesettings.js b/www/js/splash/storedevicesettings.js index 9f596e9a2..aaaf82c6b 100644 --- a/www/js/splash/storedevicesettings.js +++ b/www/js/splash/storedevicesettings.js @@ -1,84 +1,61 @@ import angular from 'angular'; -angular - .module('emission.splash.storedevicesettings', [ - 'emission.plugin.logger', - 'emission.services', - 'emission.splash.startprefs', - ]) - .factory( - 'StoreDeviceSettings', - function ( - $window, - $state, - $rootScope, - $ionicPlatform, - $ionicPopup, - Logger, - CommHelper, - StartPrefs, - ) { - var storedevicesettings = {}; +angular.module('emission.splash.storedevicesettings', ['emission.plugin.logger', + 'emission.services', + 'emission.splash.startprefs']) +.factory('StoreDeviceSettings', function($window, $state, $rootScope, $ionicPlatform, + $ionicPopup, Logger, CommHelper, StartPrefs) { - storedevicesettings.storeDeviceSettings = function () { - var lang = i18next.resolvedLanguage; - var manufacturer = $window.device.manufacturer; - var osver = $window.device.version; - return $window.cordova.getAppVersion - .getVersionNumber() - .then(function (appver) { - var updateJSON = { - phone_lang: lang, - curr_platform: ionic.Platform.platform(), - manufacturer: manufacturer, - client_os_version: osver, - client_app_version: appver, - }; - Logger.log('About to update profile with settings = ' + JSON.stringify(updateJSON)); - return CommHelper.updateUser(updateJSON); - }) - .then(function (updateJSON) { - // alert("Finished saving token = "+JSON.stringify(t.token)); - }) - .catch(function (error) { - Logger.displayError('Error in updating profile to store device settings', error); - }); - }; + var storedevicesettings = {}; - $ionicPlatform.ready().then(function () { - storedevicesettings.datacollect = $window.cordova.plugins.BEMDataCollection; - StartPrefs.readConsentState() - .then(StartPrefs.isConsented) - .then(function (consentState) { - if (consentState == true) { - storedevicesettings.storeDeviceSettings(); - } else { - Logger.log('no consent yet, waiting to store device settings in profile'); - } - }); - Logger.log('storedevicesettings startup done'); + storedevicesettings.storeDeviceSettings = function() { + var lang = i18next.resolvedLanguage; + var manufacturer = $window.device.manufacturer; + var osver = $window.device.version; + return $window.cordova.getAppVersion.getVersionNumber().then(function(appver) { + var updateJSON = { + phone_lang: lang, + curr_platform: ionic.Platform.platform(), + manufacturer: manufacturer, + client_os_version: osver, + client_app_version: appver + }; + Logger.log("About to update profile with settings = "+JSON.stringify(updateJSON)); + return CommHelper.updateUser(updateJSON); + }).then(function(updateJSON) { + // alert("Finished saving token = "+JSON.stringify(t.token)); + }).catch(function(error) { + Logger.displayError("Error in updating profile to store device settings", error); }); + } + + $ionicPlatform.ready().then(function() { + storedevicesettings.datacollect = $window.cordova.plugins.BEMDataCollection; + StartPrefs.readConsentState() + .then(StartPrefs.isConsented) + .then(function(consentState) { + if (consentState == true) { + storedevicesettings.storeDeviceSettings(); + } else { + Logger.log("no consent yet, waiting to store device settings in profile"); + } + }); + Logger.log("storedevicesettings startup done"); + }); - $rootScope.$on(StartPrefs.CONSENTED_EVENT, function (event, data) { - console.log( - 'got consented event ' + - JSON.stringify(event.name) + - ' with data ' + - JSON.stringify(data), - ); - if (StartPrefs.isIntroDone()) { - console.log('intro is done -> reconsent situation, we already have a token -> register'); + $rootScope.$on(StartPrefs.CONSENTED_EVENT, function(event, data) { + console.log("got consented event "+JSON.stringify(event.name) + +" with data "+ JSON.stringify(data)); + if (StartPrefs.isIntroDone()) { + console.log("intro is done -> reconsent situation, we already have a token -> register"); storedevicesettings.storeDeviceSettings(); - } - }); + } + }); - $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function (event, data) { - console.log( - 'intro is done -> original consent situation, we should have a token by now -> register', - ); - storedevicesettings.storeDeviceSettings(); - }); + $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function(event, data) { + console.log("intro is done -> original consent situation, we should have a token by now -> register"); + storedevicesettings.storeDeviceSettings(); + }); - return storedevicesettings; - }, - ); + return storedevicesettings; +}); diff --git a/www/js/stats/clientstats.js b/www/js/stats/clientstats.js index 43f379484..7fe4d9cb3 100644 --- a/www/js/stats/clientstats.js +++ b/www/js/stats/clientstats.js @@ -2,95 +2,91 @@ import angular from 'angular'; -angular - .module('emission.stats.clientstats', []) +angular.module('emission.stats.clientstats', []) - .factory('ClientStats', function ($window) { - var clientStat = {}; +.factory('ClientStats', function($window) { + var clientStat = {}; - clientStat.CLIENT_TIME = 'stats/client_time'; - clientStat.CLIENT_ERROR = 'stats/client_error'; - clientStat.CLIENT_NAV_EVENT = 'stats/client_nav_event'; + clientStat.CLIENT_TIME = "stats/client_time"; + clientStat.CLIENT_ERROR = "stats/client_error"; + clientStat.CLIENT_NAV_EVENT = "stats/client_nav_event"; - clientStat.getStatKeys = function () { - return { - STATE_CHANGED: 'state_changed', - BUTTON_FORCE_SYNC: 'button_sync_forced', - CHECKED_DIARY: 'checked_diary', - DIARY_TIME: 'diary_time', - METRICS_TIME: 'metrics_time', - CHECKED_INF_SCROLL: 'checked_inf_scroll', - INF_SCROLL_TIME: 'inf_scroll_time', - VERIFY_TRIP: 'verify_trip', - LABEL_TAB_SWITCH: 'label_tab_switch', - SELECT_LABEL: 'select_label', - EXPANDED_TRIP: 'expanded_trip', - NOTIFICATION_OPEN: 'notification_open', - REMINDER_PREFS: 'reminder_time_prefs', - MISSING_KEYS: 'missing_keys', - }; + clientStat.getStatKeys = function() { + return { + STATE_CHANGED: "state_changed", + BUTTON_FORCE_SYNC: "button_sync_forced", + CHECKED_DIARY: "checked_diary", + DIARY_TIME: "diary_time", + METRICS_TIME: "metrics_time", + CHECKED_INF_SCROLL: "checked_inf_scroll", + INF_SCROLL_TIME: "inf_scroll_time", + VERIFY_TRIP: "verify_trip", + LABEL_TAB_SWITCH: "label_tab_switch", + SELECT_LABEL: "select_label", + EXPANDED_TRIP: "expanded_trip", + NOTIFICATION_OPEN: "notification_open", + REMINDER_PREFS: "reminder_time_prefs", + MISSING_KEYS: "missing_keys" }; + } - clientStat.getDB = function () { - if ( - angular.isDefined($window) && - angular.isDefined($window.cordova) && - angular.isDefined($window.cordova.plugins) - ) { + clientStat.getDB = function() { + if (angular.isDefined($window) && angular.isDefined($window.cordova) && + angular.isDefined($window.cordova.plugins)) { return $window.cordova.plugins.BEMUserCache; - } else { + } else { return; // undefined - } - }; + } + } - clientStat.getAppVersion = function () { - if (angular.isDefined(clientStat.appVersion)) { + clientStat.getAppVersion = function() { + if (angular.isDefined(clientStat.appVersion)) { return clientStat.appVersion; - } else { - if ( - angular.isDefined($window) && - angular.isDefined($window.cordova) && - angular.isDefined($window.cordova.getAppVersion) - ) { - $window.cordova.getAppVersion.getVersionNumber().then(function (version) { - clientStat.appVersion = version; - }); + } else { + if (angular.isDefined($window) && angular.isDefined($window.cordova) && + angular.isDefined($window.cordova.getAppVersion)) { + $window.cordova.getAppVersion.getVersionNumber().then(function(version) { + clientStat.appVersion = version; + }); } return; - } - }; + } + } - clientStat.getStatsEvent = function (name, reading) { - var ts_sec = Date.now() / 1000; - var appVersion = clientStat.getAppVersion(); - return { - name: name, - ts: ts_sec, - reading: reading, - client_app_version: appVersion, - client_os_version: $window.device.version, - }; - }; - clientStat.addReading = function (name, reading) { - var db = clientStat.getDB(); - if (angular.isDefined(db)) { - return db.putMessage(clientStat.CLIENT_TIME, clientStat.getStatsEvent(name, reading)); - } + clientStat.getStatsEvent = function(name, reading) { + var ts_sec = Date.now() / 1000; + var appVersion = clientStat.getAppVersion(); + return { + 'name': name, + 'ts': ts_sec, + 'reading': reading, + 'client_app_version': appVersion, + 'client_os_version': $window.device.version }; + } + clientStat.addReading = function(name, reading) { + var db = clientStat.getDB(); + if (angular.isDefined(db)) { + return db.putMessage(clientStat.CLIENT_TIME, + clientStat.getStatsEvent(name, reading)); + } + } - clientStat.addEvent = function (name) { - var db = clientStat.getDB(); - if (angular.isDefined(db)) { - return db.putMessage(clientStat.CLIENT_NAV_EVENT, clientStat.getStatsEvent(name, null)); - } - }; + clientStat.addEvent = function(name) { + var db = clientStat.getDB(); + if (angular.isDefined(db)) { + return db.putMessage(clientStat.CLIENT_NAV_EVENT, + clientStat.getStatsEvent(name, null)); + } + } - clientStat.addError = function (name, errorStr) { - var db = clientStat.getDB(); - if (angular.isDefined(db)) { - return db.putMessage(clientStat.CLIENT_ERROR, clientStat.getStatsEvent(name, errorStr)); - } - }; + clientStat.addError = function(name, errorStr) { + var db = clientStat.getDB(); + if (angular.isDefined(db)) { + return db.putMessage(clientStat.CLIENT_ERROR, + clientStat.getStatsEvent(name, errorStr)); + } + } - return clientStat; - }); + return clientStat; +}) diff --git a/www/js/survey/enketo/AddNoteButton.tsx b/www/js/survey/enketo/AddNoteButton.tsx index fb35951ee..1b85c728e 100644 --- a/www/js/survey/enketo/AddNoteButton.tsx +++ b/www/js/survey/enketo/AddNoteButton.tsx @@ -7,23 +7,23 @@ The start and end times of the addition are determined by the survey response. */ -import React, { useEffect, useState, useContext } from 'react'; -import DiaryButton from '../../components/DiaryButton'; -import { useTranslation } from 'react-i18next'; -import moment from 'moment'; -import { LabelTabContext } from '../../diary/LabelTab'; -import EnketoModal from './EnketoModal'; -import { displayErrorMsg, logDebug } from '../../plugin/logger'; +import React, { useEffect, useState, useContext } from "react"; +import DiaryButton from "../../components/DiaryButton"; +import { useTranslation } from "react-i18next"; +import moment from "moment"; +import { LabelTabContext } from "../../diary/LabelTab"; +import EnketoModal from "./EnketoModal"; +import { displayErrorMsg, logDebug } from "../../plugin/logger"; type Props = { - timelineEntry: any; - notesConfig: any; - storeKey: string; -}; + timelineEntry: any, + notesConfig: any, + storeKey: string, +} const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { const { t, i18n } = useTranslation(); const [displayLabel, setDisplayLabel] = useState(''); - const { repopulateTimelineEntry } = useContext(LabelTabContext); + const { repopulateTimelineEntry } = useContext(LabelTabContext) useEffect(() => { let newLabel: string; @@ -39,19 +39,20 @@ const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { // return a dictionary of fields we want to prefill, using start/enter and end/exit times function getPrefillTimes() { + let begin = timelineEntry.start_ts || timelineEntry.enter_ts; let stop = timelineEntry.end_ts || timelineEntry.exit_ts; // if addition(s) already present on this timeline entry, `begin` where the last one left off - timelineEntry.additionsList.forEach((a) => { - if (a.data.end_ts > (begin || 0) && a.data.end_ts != stop) begin = a.data.end_ts; + timelineEntry.additionsList.forEach(a => { + if (a.data.end_ts > (begin || 0) && a.data.end_ts != stop) + begin = a.data.end_ts; }); - - const timezone = - timelineEntry.start_local_dt?.timezone || - timelineEntry.enter_local_dt?.timezone || - timelineEntry.end_local_dt?.timezone || - timelineEntry.exit_local_dt?.timezone; + + const timezone = timelineEntry.start_local_dt?.timezone + || timelineEntry.enter_local_dt?.timezone + || timelineEntry.end_local_dt?.timezone + || timelineEntry.exit_local_dt?.timezone; const momentBegin = begin ? moment(begin * 1000).tz(timezone) : null; const momentStop = stop ? moment(stop * 1000).tz(timezone) : null; @@ -79,14 +80,11 @@ const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { console.log('About to launch survey ', surveyName); setPrefillTimes(getPrefillTimes()); setModalVisible(true); - } + }; function onResponseSaved(result) { if (result) { - logDebug( - 'AddNoteButton: response was saved, about to repopulateTimelineEntry; result=' + - JSON.stringify(result), - ); + logDebug('AddNoteButton: response was saved, about to repopulateTimelineEntry; result=' + JSON.stringify(result)); repopulateTimelineEntry(timelineEntry._id.$oid); } else { displayErrorMsg('AddNoteButton: response was not saved, result=', result); @@ -96,20 +94,19 @@ const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { const [prefillTimes, setPrefillTimes] = useState(null); const [modalVisible, setModalVisible] = useState(false); - return ( - <> - launchAddNoteSurvey()}> - {displayLabel} - - setModalVisible(false)} - onResponseSaved={onResponseSaved} - surveyName={notesConfig?.surveyName} - opts={{ timelineEntry, dataKey: storeKey, prefillFields: prefillTimes }} - /> - - ); + return (<> + launchAddNoteSurvey()}> + {displayLabel} + + setModalVisible(false)} + onResponseSaved={onResponseSaved} + surveyName={notesConfig?.surveyName} + opts={{ timelineEntry, + dataKey: storeKey, + prefillFields: prefillTimes + }} /> + ); }; export default AddNoteButton; diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index 8abf033d3..b4bf8f024 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -10,12 +10,13 @@ import { displayError, displayErrorMsg } from '../../plugin/logger'; // import { transform } from 'enketo-transformer/web'; type Props = Omit & { - surveyName: string; - onResponseSaved: (response: any) => void; - opts?: SurveyOptions; -}; + surveyName: string, + onResponseSaved: (response: any) => void, + opts?: SurveyOptions, +} + +const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => { -const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const { t, i18n } = useTranslation(); const headerEl = useRef(null); const surveyJson = useRef(null); @@ -26,11 +27,9 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const responseText = await fetchUrlCached(url); try { return JSON.parse(responseText); - } catch ({ name, message }) { + } catch ({name, message}) { // not JSON, so it must be XML - return Promise.reject( - 'downloaded survey was not JSON; enketo-transformer is not available yet', - ); + return Promise.reject('downloaded survey was not JSON; enketo-transformer is not available yet'); /* uncomment once enketo-transformer is available */ // if `response` is not JSON, it is an XML string and needs transformation to JSON // const xmlText = await res.text(); @@ -42,21 +41,18 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const valid = await enketoForm.current.validate(); if (!valid) return false; const result = await saveResponse(surveyName, enketoForm.current, appConfig, opts); - if (!result) { - // validation failed + if (!result) { // validation failed displayErrorMsg(t('survey.enketo-form-errors')); - } else if (result instanceof Error) { - // error thrown in saveResponse + } else if (result instanceof Error) { // error thrown in saveResponse displayError(result); - } else { - // success + } else { // success rest.onDismiss(); onResponseSaved(result); return; } } - // init logic: retrieve form -> inject into DOM -> initialize Enketo -> show modal + // init logic: retrieve form -> inject into DOM -> initialize Enketo -> show modal function initSurvey() { console.debug('Loading survey', surveyName); const formPath = appConfig.survey_info?.surveys?.[surveyName]?.formPath; @@ -95,14 +91,10 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => {
- - Choose Language - + Choose Language