diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index aca25232d..000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -insert_final_newline = false -trim_trailing_whitespace = false \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..be7b1726d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# Ignore www/dist, manual_lib, json +www/dist +www/manual_lib +www/json + +# This is the pattern to check only www directory +# Ignore all +/* +# but don't ignore all the files in www directory +!/www diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..5875d605a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "bracketSameLine": true, + "endOfLine": "lf", + "semi": true +} diff --git a/package.serve.json b/package.serve.json index 8da81f941..6315b6a46 100644 --- a/package.serve.json +++ b/package.serve.json @@ -49,7 +49,8 @@ "typescript": "^5.0.3", "url-loader": "^4.1.1", "webpack": "^5.0.1", - "webpack-cli": "^5.0.1" + "webpack-cli": "^5.0.1", + "prettier": "3.0.3" }, "dependencies": { "@react-navigation/native": "^6.1.7", diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index c00377120..62aa9be1a 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -5,13 +5,13 @@ export const mockCordova = () => { window['cordova'].platformId ||= 'ios'; window['cordova'].platformVersion ||= packageJsonBuild.dependencies['cordova-ios']; window['cordova'].plugins ||= {}; -} +}; export const mockDevice = () => { window['device'] ||= {}; window['device'].platform ||= 'ios'; window['device'].version ||= '14.0.0'; -} +}; export const mockGetAppVersion = () => { const mockGetAppVersion = { @@ -19,15 +19,17 @@ export const mockGetAppVersion = () => { getPackageName: () => new Promise((rs, rj) => setTimeout(() => rs('com.example.mockapp'), 10)), getVersionCode: () => new Promise((rs, rj) => setTimeout(() => rs('123'), 10)), getVersionNumber: () => new Promise((rs, rj) => setTimeout(() => rs('1.2.3'), 10)), - } + }; window['cordova'] ||= {}; window['cordova'].getAppVersion = mockGetAppVersion; -} +}; export const mockFile = () => { - window['cordova'].file = { "dataDirectory" : "../path/to/data/directory", - "applicationStorageDirectory" : "../path/to/app/storage/directory"}; -} + window['cordova'].file = { + dataDirectory: '../path/to/data/directory', + applicationStorageDirectory: '../path/to/app/storage/directory', + }; +}; //for consent document const _storage = {}; @@ -40,7 +42,7 @@ export const mockBEMUserCache = () => { return new Promise((rs, rj) => setTimeout(() => { rs(_cache[key]); - }, 100) + }, 100), ); }, putLocalStorage: (key: string, value: any) => { @@ -48,7 +50,7 @@ export const mockBEMUserCache = () => { setTimeout(() => { _cache[key] = value; rs(); - }, 100) + }, 100), ); }, removeLocalStorage: (key: string) => { @@ -56,7 +58,7 @@ export const mockBEMUserCache = () => { setTimeout(() => { delete _cache[key]; rs(); - }, 100) + }, 100), ); }, clearAll: () => { @@ -64,21 +66,21 @@ export const mockBEMUserCache = () => { setTimeout(() => { for (let p in _cache) delete _cache[p]; rs(); - }, 100) + }, 100), ); }, listAllLocalStorageKeys: () => { return new Promise((rs, rj) => setTimeout(() => { rs(Object.keys(_cache)); - }, 100) + }, 100), ); }, listAllUniqueKeys: () => { return new Promise((rs, rj) => setTimeout(() => { rs(Object.keys(_cache)); - }, 100) + }, 100), ); }, putMessage: (key: string, value: any) => { @@ -86,46 +88,48 @@ export const mockBEMUserCache = () => { setTimeout(() => { messages.push({ key, value }); rs(); - }, 100) + }, 100), ); }, getAllMessages: (key: string, withMetadata?: boolean) => { return new Promise((rs, rj) => setTimeout(() => { - rs(messages.filter(m => m.key == key).map(m => m.value)); - }, 100) + rs(messages.filter((m) => m.key == key).map((m) => m.value)); + }, 100), ); }, getDocument: (key: string, withMetadata?: boolean) => { return new Promise((rs, rj) => setTimeout(() => { rs(_storage[key]); - }, 100) + }, 100), ); }, isEmptyDoc: (doc) => { - if (doc == undefined) { return true } + if (doc == undefined) { + return true; + } let string = doc.toString(); if (string.length == 0) { return true; } else { return false; } - } - } + }, + }; window['cordova'] ||= {}; window['cordova'].plugins ||= {}; window['cordova'].plugins.BEMUserCache = mockBEMUserCache; -} +}; export const mockBEMDataCollection = () => { const mockBEMDataCollection = { markConsented: (consentDoc) => { setTimeout(() => { _storage['config/consent'] = consentDoc; - }, 100) - } - } + }, 100); + }, + }; window['cordova'] ||= {}; window['cordova'].plugins.BEMDataCollection = mockBEMDataCollection; -} +}; diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts index d7c2743ac..70b532507 100644 --- a/www/__mocks__/fileSystemMocks.ts +++ b/www/__mocks__/fileSystemMocks.ts @@ -1,23 +1,21 @@ export const mockFileSystem = () => { window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { const fs = { - filesystem: - { - root: - { + filesystem: { + root: { getFile: (path, options, onSuccess) => { let fileEntry = { file: (handleFile) => { - let file = new File(["this is a mock"], "loggerDB"); + let file = new File(['this is a mock'], 'loggerDB'); handleFile(file); - } - } + }, + }; onSuccess(fileEntry); - } - } - } - } - console.log("in mock, fs is ", fs, " get File is ", fs.filesystem.root.getFile); + }, + }, + }, + }; + console.log('in mock, fs is ', fs, ' get File is ', fs.filesystem.root.getFile); handleFS(fs); - } -} \ No newline at end of file + }; +}; diff --git a/www/__mocks__/globalMocks.ts b/www/__mocks__/globalMocks.ts index 3d9b71507..f13cb274b 100644 --- a/www/__mocks__/globalMocks.ts +++ b/www/__mocks__/globalMocks.ts @@ -1,3 +1,3 @@ export const mockLogger = () => { window['Logger'] = { log: console.log }; -} +}; diff --git a/www/__tests__/LoadMoreButton.test.tsx b/www/__tests__/LoadMoreButton.test.tsx index 5acb4a700..100cf19fc 100644 --- a/www/__tests__/LoadMoreButton.test.tsx +++ b/www/__tests__/LoadMoreButton.test.tsx @@ -1,30 +1,23 @@ /** * @jest-environment jsdom */ -import React from 'react' -import {render, fireEvent, waitFor, screen} from '@testing-library/react-native' -import LoadMoreButton from '../js/diary/list/LoadMoreButton' +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react-native'; +import LoadMoreButton from '../js/diary/list/LoadMoreButton'; - -describe("LoadMoreButton", () => { - it("renders correctly", async () => { - render( - {}}>{} - ); +describe('LoadMoreButton', () => { + it('renders correctly', async () => { + render( {}}>{}); await waitFor(() => { - expect(screen.getByTestId("load-button")).toBeTruthy(); + expect(screen.getByTestId('load-button')).toBeTruthy(); }); }); - it("calls onPressFn when clicked", () => { + it('calls onPressFn when clicked', () => { const mockFn = jest.fn(); - const { getByTestId } = render( - {} - ); - const loadButton = getByTestId("load-button"); + const { getByTestId } = render({}); + const loadButton = getByTestId('load-button'); fireEvent.press(loadButton); expect(mockFn).toHaveBeenCalled(); }); }); - - diff --git a/www/__tests__/clientStats.test.ts b/www/__tests__/clientStats.test.ts index d1a054195..a3a953582 100644 --- a/www/__tests__/clientStats.test.ts +++ b/www/__tests__/clientStats.test.ts @@ -1,5 +1,11 @@ -import { mockBEMUserCache, mockDevice, mockGetAppVersion } from "../__mocks__/cordovaMocks"; -import { addStatError, addStatEvent, addStatReading, getAppVersion, statKeys } from "../js/plugin/clientStats"; +import { mockBEMUserCache, mockDevice, mockGetAppVersion } from '../__mocks__/cordovaMocks'; +import { + addStatError, + addStatEvent, + addStatReading, + getAppVersion, + statKeys, +} from '../js/plugin/clientStats'; mockDevice(); // this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3" @@ -22,7 +28,7 @@ it('stores a client stats reading', async () => { ts: expect.any(Number), reading, client_app_version: '1.2.3', - client_os_version: '14.0.0' + client_os_version: '14.0.0', }); }); @@ -34,7 +40,7 @@ it('stores a client stats event', async () => { ts: expect.any(Number), reading: null, client_app_version: '1.2.3', - client_os_version: '14.0.0' + client_os_version: '14.0.0', }); }); @@ -47,6 +53,6 @@ it('stores a client stats error', async () => { ts: expect.any(Number), reading: errorStr, client_app_version: '1.2.3', - client_os_version: '14.0.0' + client_os_version: '14.0.0', }); }); diff --git a/www/__tests__/commHelper.test.ts b/www/__tests__/commHelper.test.ts index 2e2dfc6af..8bc52a408 100644 --- a/www/__tests__/commHelper.test.ts +++ b/www/__tests__/commHelper.test.ts @@ -5,19 +5,27 @@ mockLogger(); // mock for JavaScript 'fetch' // we emulate a 100ms delay when i) fetching data and ii) parsing it as text -global.fetch = (url: string) => new Promise((rs, rj) => { - setTimeout(() => rs({ - text: () => new Promise((rs, rj) => { - setTimeout(() => rs('mock data for ' + url), 100); - }) - })); -}) as any; +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + setTimeout(() => rs('mock data for ' + url), 100); + }), + }), + ); + }) as any; it('fetches text from a URL and caches it so the next call is faster', async () => { const tsBeforeCalls = Date.now(); - const text1 = await fetchUrlCached('https://raw.githubusercontent.com/e-mission/e-mission-phone/master/README.md'); + const text1 = await fetchUrlCached( + 'https://raw.githubusercontent.com/e-mission/e-mission-phone/master/README.md', + ); const tsBetweenCalls = Date.now(); - const text2 = await fetchUrlCached('https://raw.githubusercontent.com/e-mission/e-mission-phone/master/README.md'); + const text2 = await fetchUrlCached( + 'https://raw.githubusercontent.com/e-mission/e-mission-phone/master/README.md', + ); const tsAfterCalls = Date.now(); expect(text1).toEqual(expect.stringContaining('mock data')); expect(text2).toEqual(expect.stringContaining('mock data')); diff --git a/www/__tests__/customURL.test.ts b/www/__tests__/customURL.test.ts index 68ce3c47d..c06345679 100644 --- a/www/__tests__/customURL.test.ts +++ b/www/__tests__/customURL.test.ts @@ -1,38 +1,38 @@ import { onLaunchCustomURL } from '../js/splash/customURL'; describe('onLaunchCustomURL', () => { - let mockHandler; + let mockHandler; - beforeEach(() => { - // create a new mock handler before each test case. - mockHandler = jest.fn(); - }); + beforeEach(() => { + // create a new mock handler before each test case. + mockHandler = jest.fn(); + }); - it('tests valid url 1 - should call handler callback with valid URL and the handler should be called with correct parameters', () => { - const validURL = 'emission://login_token?token=nrelop_dev-emulator-program'; - const expectedURL = 'login_token?token=nrelop_dev-emulator-program'; - const expectedComponents = { route: 'login_token', token: 'nrelop_dev-emulator-program' }; - onLaunchCustomURL(validURL, mockHandler); - expect(mockHandler).toHaveBeenCalledWith(expectedURL, expectedComponents); - }); + it('tests valid url 1 - should call handler callback with valid URL and the handler should be called with correct parameters', () => { + const validURL = 'emission://login_token?token=nrelop_dev-emulator-program'; + const expectedURL = 'login_token?token=nrelop_dev-emulator-program'; + const expectedComponents = { route: 'login_token', token: 'nrelop_dev-emulator-program' }; + onLaunchCustomURL(validURL, mockHandler); + expect(mockHandler).toHaveBeenCalledWith(expectedURL, expectedComponents); + }); - it('tests valid url 2 - should call handler callback with valid URL and the handler should be called with correct parameters', () => { - const validURL = 'emission://test?param1=first¶m2=second'; - const expectedURL = 'test?param1=first¶m2=second'; - const expectedComponents = { route: 'test', param1: 'first', param2: 'second' }; - onLaunchCustomURL(validURL, mockHandler); - expect(mockHandler).toHaveBeenCalledWith(expectedURL, expectedComponents); - }); + it('tests valid url 2 - should call handler callback with valid URL and the handler should be called with correct parameters', () => { + const validURL = 'emission://test?param1=first¶m2=second'; + const expectedURL = 'test?param1=first¶m2=second'; + const expectedComponents = { route: 'test', param1: 'first', param2: 'second' }; + onLaunchCustomURL(validURL, mockHandler); + expect(mockHandler).toHaveBeenCalledWith(expectedURL, expectedComponents); + }); - it('test invalid url 1 - should not call handler callback with invalid URL', () => { - const invalidURL = 'invalid_url'; - onLaunchCustomURL(invalidURL, mockHandler); - expect(mockHandler).not.toHaveBeenCalled(); - }); + it('test invalid url 1 - should not call handler callback with invalid URL', () => { + const invalidURL = 'invalid_url'; + onLaunchCustomURL(invalidURL, mockHandler); + expect(mockHandler).not.toHaveBeenCalled(); + }); - it('tests invalid url 2 - should not call handler callback with invalid URL', () => { - const invalidURL = ''; - onLaunchCustomURL(invalidURL, mockHandler); - expect(mockHandler).not.toHaveBeenCalled(); - }); -}) \ No newline at end of file + it('tests invalid url 2 - should not call handler callback with invalid URL', () => { + const invalidURL = ''; + onLaunchCustomURL(invalidURL, mockHandler); + expect(mockHandler).not.toHaveBeenCalled(); + }); +}); diff --git a/www/__tests__/diaryHelper.test.ts b/www/__tests__/diaryHelper.test.ts index 822b19bba..1ac143334 100644 --- a/www/__tests__/diaryHelper.test.ts +++ b/www/__tests__/diaryHelper.test.ts @@ -1,63 +1,86 @@ -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/__tests__/startprefs.test.ts b/www/__tests__/startprefs.test.ts index 1e62e7b5e..75ed707dc 100644 --- a/www/__tests__/startprefs.test.ts +++ b/www/__tests__/startprefs.test.ts @@ -1,24 +1,41 @@ -import { markConsented, isConsented, readConsentState, getConsentDocument } from '../js/splash/startprefs'; +import { + markConsented, + isConsented, + readConsentState, + getConsentDocument, +} from '../js/splash/startprefs'; -import { mockBEMUserCache, mockBEMDataCollection } from "../__mocks__/cordovaMocks"; -import { mockLogger } from "../__mocks__/globalMocks"; +import { mockBEMUserCache, mockBEMDataCollection } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; mockBEMUserCache(); mockBEMDataCollection(); mockLogger(); -global.fetch = (url: string) => new Promise((rs, rj) => { - setTimeout(() => rs({ - json: () => new Promise((rs, rj) => { - let myJSON = { "emSensorDataCollectionProtocol": { "protocol_id": "2014-04-6267", "approval_date": "2016-07-14" } }; - setTimeout(() => rs(myJSON), 100); - }) - })); -}) as any; +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + json: () => + new Promise((rs, rj) => { + let myJSON = { + emSensorDataCollectionProtocol: { + protocol_id: '2014-04-6267', + approval_date: '2016-07-14', + }, + }; + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; it('checks state of consent before and after marking consent', async () => { expect(await readConsentState().then(isConsented)).toBeFalsy(); let marked = await markConsented(); expect(await readConsentState().then(isConsented)).toBeTruthy(); - expect(await getConsentDocument()).toEqual({"approval_date": "2016-07-14", "protocol_id": "2014-04-6267"}); -}); \ No newline at end of file + expect(await getConsentDocument()).toEqual({ + approval_date: '2016-07-14', + protocol_id: '2014-04-6267', + }); +}); diff --git a/www/__tests__/storage.test.ts b/www/__tests__/storage.test.ts index 6fea4f8b9..ca6d71dec 100644 --- a/www/__tests__/storage.test.ts +++ b/www/__tests__/storage.test.ts @@ -1,6 +1,6 @@ -import { mockBEMUserCache } from "../__mocks__/cordovaMocks"; -import { mockLogger } from "../__mocks__/globalMocks"; -import { storageClear, storageGet, storageRemove, storageSet } from "../js/plugin/storage"; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import { storageClear, storageGet, storageRemove, storageSet } from '../js/plugin/storage'; // mocks used - storage.ts uses BEMUserCache and logging. // localStorage is already mocked for us by Jest :) diff --git a/www/__tests__/uploadService.test.ts b/www/__tests__/uploadService.test.ts index 5c64fae0e..b9bede9fd 100644 --- a/www/__tests__/uploadService.test.ts +++ b/www/__tests__/uploadService.test.ts @@ -2,51 +2,55 @@ //however, tests are still important to make sure the code works //at some point we hope to restore this functionality -import {uploadFile} from "../js/control/uploadService"; +import { uploadFile } from '../js/control/uploadService'; import { mockLogger } from '../__mocks__/globalMocks'; -import { mockDevice, mockGetAppVersion, mockCordova, mockFile } from "../__mocks__/cordovaMocks"; -import { mockFileSystem } from "../__mocks__/fileSystemMocks"; +import { mockDevice, mockGetAppVersion, mockCordova, mockFile } from '../__mocks__/cordovaMocks'; +import { mockFileSystem } from '../__mocks__/fileSystemMocks'; mockDevice(); mockGetAppVersion(); mockCordova(); mockLogger(); -mockFile(); //mocks the base directory +mockFile(); //mocks the base directory mockFileSystem(); //comnplex mock, allows the readDBFile to work in testing //use this message to verify that the post went through -let message = ""; +let message = ''; //each have a slight delay to mimic a real fetch request -global.fetch = (url: string, options: { method: string, headers: {}, body: string }) => new Promise((rs, rj) => { - //if there's options, that means there is a post request - if (options) { - message = "sent " + options.method + options.body + " for " + url; - setTimeout(() => { - rs('sent ' + options.method + options.body + ' to ' + url); - }, 100); - } - //else it is a get request - else { - setTimeout(() => rs({ - json: () => new Promise((rs, rj) => { - setTimeout(() => rs('mock data for ' + url), 100); - }) - })); - } -}) as any; +global.fetch = (url: string, options: { method: string; headers: {}; body: string }) => + new Promise((rs, rj) => { + //if there's options, that means there is a post request + if (options) { + message = 'sent ' + options.method + options.body + ' for ' + url; + setTimeout(() => { + rs('sent ' + options.method + options.body + ' to ' + url); + }, 100); + } + //else it is a get request + else { + setTimeout(() => + rs({ + json: () => + new Promise((rs, rj) => { + setTimeout(() => rs('mock data for ' + url), 100); + }), + }), + ); + } + }) as any; window.alert = (message) => { console.log(message); -} +}; //very basic tests - difficult to do too much since there's a lot of mocking involved it('posts the logs to the configured database', async () => { - let posted = await uploadFile("loggerDB", "HelloWorld"); - expect(message).toEqual(expect.stringContaining("HelloWorld")); - expect(message).toEqual(expect.stringContaining("POST")); - posted = await uploadFile("loggerDB", "second test"); - expect(message).toEqual(expect.stringContaining("second test")); - expect(message).toEqual(expect.stringContaining("POST")); -}, 10000); \ No newline at end of file + let posted = await uploadFile('loggerDB', 'HelloWorld'); + expect(message).toEqual(expect.stringContaining('HelloWorld')); + expect(message).toEqual(expect.stringContaining('POST')); + posted = await uploadFile('loggerDB', 'second test'); + expect(message).toEqual(expect.stringContaining('second test')); + expect(message).toEqual(expect.stringContaining('POST')); +}, 10000); diff --git a/www/__tests__/useImperialConfig.test.ts b/www/__tests__/useImperialConfig.test.ts index cab1b5a11..593498aae 100644 --- a/www/__tests__/useImperialConfig.test.ts +++ b/www/__tests__/useImperialConfig.test.ts @@ -1,16 +1,15 @@ import { convertDistance, convertSpeed, formatForDisplay } from '../js/config/useImperialConfig'; - // This mock is required, or else the test will dive into the import chain of useAppConfig.ts and fail when it gets to the root jest.mock('../js/useAppConfig', () => { return jest.fn(() => ({ appConfig: { - use_imperial: false + use_imperial: false, }, - loading: false + loading: false, })); }); - + describe('formatForDisplay', () => { it('should round to the nearest integer when value is >= 100', () => { expect(formatForDisplay(105)).toBe('105'); diff --git a/www/build/app.css b/www/build/app.css index d7ec98c10..97de0161f 100644 --- a/www/build/app.css +++ b/www/build/app.css @@ -19,7 +19,58 @@ * ======================================================================== */ -.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8;filter:alpha(opacity=80)}.tour-step-backdrop{position:relative;z-index:1101;background:inherit}.tour-step-backdrop>td{position:relative;z-index:1101}.tour-step-background{position:absolute!important;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1100}.popover[class*=tour-] .popover-navigation{padding:9px 14px}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none} +.tour-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1100; + background-color: #000; + opacity: 0.8; + filter: alpha(opacity=80); +} +.tour-step-backdrop { + position: relative; + z-index: 1101; + background: inherit; +} +.tour-step-backdrop > td { + position: relative; + z-index: 1101; +} +.tour-step-background { + position: absolute !important; + z-index: 1100; + background: inherit; + border-radius: 6px; +} +.popover[class*='tour-'] { + z-index: 1100; +} +.popover[class*='tour-'] .popover-navigation { + padding: 9px 14px; +} +.popover[class*='tour-'] .popover-navigation [data-role='end'] { + float: right; +} +.popover[class*='tour-'] .popover-navigation [data-role='prev'], +.popover[class*='tour-'] .popover-navigation [data-role='next'], +.popover[class*='tour-'] .popover-navigation [data-role='end'] { + cursor: pointer; +} +.popover[class*='tour-'] .popover-navigation [data-role='prev'].disabled, +.popover[class*='tour-'] .popover-navigation [data-role='next'].disabled, +.popover[class*='tour-'] .popover-navigation [data-role='end'].disabled { + cursor: default; +} +.popover[class*='tour-'].orphan { + position: fixed; + margin-top: 0; +} +.popover[class*='tour-'].orphan .arrow { + display: none; +} /*! * angular-loading-bar v0.6.0 * https://chieffancypants.github.io/angular-loading-bar @@ -76,7 +127,7 @@ right: 0; top: 0; height: 2px; - opacity: .45; + opacity: 0.45; -moz-box-shadow: #29d 1px 0 6px 1px; -ms-box-shadow: #29d 1px 0 6px 1px; -webkit-box-shadow: #29d 1px 0 6px 1px; @@ -98,50 +149,80 @@ width: 14px; height: 14px; - border: solid 2px transparent; - border-top-color: #29d; + border: solid 2px transparent; + border-top-color: #29d; border-left-color: #29d; border-radius: 10px; -webkit-animation: loading-bar-spinner 400ms linear infinite; - -moz-animation: loading-bar-spinner 400ms linear infinite; - -ms-animation: loading-bar-spinner 400ms linear infinite; - -o-animation: loading-bar-spinner 400ms linear infinite; - animation: loading-bar-spinner 400ms linear infinite; + -moz-animation: loading-bar-spinner 400ms linear infinite; + -ms-animation: loading-bar-spinner 400ms linear infinite; + -o-animation: loading-bar-spinner 400ms linear infinite; + animation: loading-bar-spinner 400ms linear infinite; } @-webkit-keyframes loading-bar-spinner { - 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } @-moz-keyframes loading-bar-spinner { - 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } } @-o-keyframes loading-bar-spinner { - 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } } @-ms-keyframes loading-bar-spinner { - 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } } @keyframes loading-bar-spinner { - 0% { transform: rotate(0deg); transform: rotate(0deg); } - 100% { transform: rotate(360deg); transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + transform: rotate(360deg); + } } /* Version: 3.4.8 Timestamp: Thu May 1 09:50:32 EDT 2014 */ .select2-container { - margin: 0; - position: relative; - display: inline-block; - /* inline-block for ie7 */ - zoom: 1; - *display: inline; - vertical-align: middle; + margin: 0; + position: relative; + display: inline-block; + /* inline-block for ie7 */ + zoom: 1; + *display: inline; + vertical-align: middle; } .select2-container, @@ -154,379 +235,438 @@ Version: 3.4.8 Timestamp: Thu May 1 09:50:32 EDT 2014 More Info : http://www.quirksmode.org/css/box.html */ -webkit-box-sizing: border-box; /* webkit */ - -moz-box-sizing: border-box; /* firefox */ - box-sizing: border-box; /* css3 */ + -moz-box-sizing: border-box; /* firefox */ + box-sizing: border-box; /* css3 */ } .select2-container .select2-choice { - display: block; - height: 26px; - padding: 0 0 0 8px; - overflow: hidden; - position: relative; + display: block; + height: 26px; + padding: 0 0 0 8px; + overflow: hidden; + position: relative; - border: 1px solid #aaa; - white-space: nowrap; - line-height: 26px; - color: #444; - text-decoration: none; + border: 1px solid #aaa; + white-space: nowrap; + line-height: 26px; + color: #444; + text-decoration: none; - border-radius: 4px; + border-radius: 4px; - background-clip: padding-box; + background-clip: padding-box; - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; - background-color: #fff; - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff)); - background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%); - background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); - background-image: linear-gradient(to top, #eee 0%, #fff 50%); + background-color: #fff; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #eee), + color-stop(0.5, #fff) + ); + background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%); + background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); + background-image: linear-gradient(to top, #eee 0%, #fff 50%); } .select2-container.select2-drop-above .select2-choice { - border-bottom-color: #aaa; + border-bottom-color: #aaa; - border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff)); - background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%); - background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); - background-image: linear-gradient(to bottom, #eee 0%, #fff 90%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #eee), + color-stop(0.9, #fff) + ); + background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%); + background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); + background-image: linear-gradient(to bottom, #eee 0%, #fff 90%); } .select2-container.select2-allowclear .select2-choice .select2-chosen { - margin-right: 42px; + margin-right: 42px; } .select2-container .select2-choice > .select2-chosen { - margin-right: 26px; - display: block; - overflow: hidden; + margin-right: 26px; + display: block; + overflow: hidden; - white-space: nowrap; + white-space: nowrap; - text-overflow: ellipsis; - float: none; - width: auto; + text-overflow: ellipsis; + float: none; + width: auto; } .select2-container .select2-choice abbr { - display: none; - width: 12px; - height: 12px; - position: absolute; - right: 24px; - top: 8px; + display: none; + width: 12px; + height: 12px; + position: absolute; + right: 24px; + top: 8px; - font-size: 1px; - text-decoration: none; + font-size: 1px; + text-decoration: none; - border: 0; - background: url('select2.png') right top no-repeat; - cursor: pointer; - outline: 0; + border: 0; + background: url('select2.png') right top no-repeat; + cursor: pointer; + outline: 0; } .select2-container.select2-allowclear .select2-choice abbr { - display: inline-block; + display: inline-block; } .select2-container .select2-choice abbr:hover { - background-position: right -11px; - cursor: pointer; + background-position: right -11px; + cursor: pointer; } .select2-drop-mask { - border: 0; - margin: 0; - padding: 0; - position: fixed; - left: 0; - top: 0; - min-height: 100%; - min-width: 100%; - height: auto; - width: auto; - opacity: 0; - z-index: 9998; - /* styles required for IE to work */ - background-color: #fff; - filter: alpha(opacity=0); + border: 0; + margin: 0; + padding: 0; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 9998; + /* styles required for IE to work */ + background-color: #fff; + filter: alpha(opacity=0); } .select2-drop { - width: 100%; - margin-top: -1px; - position: absolute; - z-index: 9999; - top: 100%; + width: 100%; + margin-top: -1px; + position: absolute; + z-index: 9999; + top: 100%; - background: #fff; - color: #000; - border: 1px solid #aaa; - border-top: 0; + background: #fff; + color: #000; + border: 1px solid #aaa; + border-top: 0; - border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; - -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); - box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); } .select2-drop.select2-drop-above { - margin-top: 1px; - border-top: 1px solid #aaa; - border-bottom: 0; + margin-top: 1px; + border-top: 1px solid #aaa; + border-bottom: 0; - border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; - -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); - box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); + -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15); + box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15); } .select2-drop-active { - border: 1px solid #5897fb; - border-top: none; + border: 1px solid #5897fb; + border-top: none; } .select2-drop.select2-drop-above.select2-drop-active { - border-top: 1px solid #5897fb; + border-top: 1px solid #5897fb; } .select2-drop-auto-width { - border-top: 1px solid #aaa; - width: auto; + border-top: 1px solid #aaa; + width: auto; } .select2-drop-auto-width .select2-search { - padding-top: 4px; + padding-top: 4px; } .select2-container .select2-choice .select2-arrow { - display: inline-block; - width: 18px; - height: 100%; - position: absolute; - right: 0; - top: 0; + display: inline-block; + width: 18px; + height: 100%; + position: absolute; + right: 0; + top: 0; - border-left: 1px solid #aaa; - border-radius: 0 4px 4px 0; + border-left: 1px solid #aaa; + border-radius: 0 4px 4px 0; - background-clip: padding-box; + background-clip: padding-box; - background: #ccc; - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); - background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); - background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); - background-image: linear-gradient(to top, #ccc 0%, #eee 60%); + background: #ccc; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #ccc), + color-stop(0.6, #eee) + ); + background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); + background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); + background-image: linear-gradient(to top, #ccc 0%, #eee 60%); } .select2-container .select2-choice .select2-arrow b { - display: block; - width: 100%; - height: 100%; - background: url('select2.png') no-repeat 0 1px; + display: block; + width: 100%; + height: 100%; + background: url('select2.png') no-repeat 0 1px; } .select2-search { - display: inline-block; - width: 100%; - min-height: 26px; - margin: 0; - padding-left: 4px; - padding-right: 4px; + display: inline-block; + width: 100%; + min-height: 26px; + margin: 0; + padding-left: 4px; + padding-right: 4px; - position: relative; - z-index: 10000; + position: relative; + z-index: 10000; - white-space: nowrap; + white-space: nowrap; } .select2-search input { - width: 100%; - height: auto !important; - min-height: 26px; - padding: 4px 20px 4px 5px; - margin: 0; + width: 100%; + height: auto !important; + min-height: 26px; + padding: 4px 20px 4px 5px; + margin: 0; - outline: 0; - font-family: sans-serif; - font-size: 1em; + outline: 0; + font-family: sans-serif; + font-size: 1em; - border: 1px solid #aaa; - border-radius: 0; + border: 1px solid #aaa; + border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; - background: #fff url('select2.png') no-repeat 100% -22px; - background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); - background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); - background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); - background: url('select2.png') no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; + background: #fff url('select2.png') no-repeat 100% -22px; + background: + url('select2.png') no-repeat 100% -22px, + -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: + url('select2.png') no-repeat 100% -22px, + -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: + url('select2.png') no-repeat 100% -22px, + -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: + url('select2.png') no-repeat 100% -22px, + linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; } .select2-drop.select2-drop-above .select2-search input { - margin-top: 4px; + margin-top: 4px; } .select2-search input.select2-active { - background: #fff url('select2-spinner.gif') no-repeat 100%; - background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); - background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); - background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); - background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; + background: #fff url('select2-spinner.gif') no-repeat 100%; + background: + url('select2-spinner.gif') no-repeat 100%, + -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: + url('select2-spinner.gif') no-repeat 100%, + -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: + url('select2-spinner.gif') no-repeat 100%, + -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: + url('select2-spinner.gif') no-repeat 100%, + linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; } .select2-container-active .select2-choice, .select2-container-active .select2-choices { - border: 1px solid #5897fb; - outline: none; + border: 1px solid #5897fb; + outline: none; - -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); - box-shadow: 0 0 5px rgba(0, 0, 0, .3); + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); } .select2-dropdown-open .select2-choice { - border-bottom-color: transparent; - -webkit-box-shadow: 0 1px 0 #fff inset; - box-shadow: 0 1px 0 #fff inset; + border-bottom-color: transparent; + -webkit-box-shadow: 0 1px 0 #fff inset; + box-shadow: 0 1px 0 #fff inset; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; - background-color: #eee; - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee)); - background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%); - background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); - background-image: linear-gradient(to top, #fff 0%, #eee 50%); + background-color: #eee; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, #fff), + color-stop(0.5, #eee) + ); + background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%); + background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); + background-image: linear-gradient(to top, #fff 0%, #eee 50%); } .select2-dropdown-open.select2-drop-above .select2-choice, .select2-dropdown-open.select2-drop-above .select2-choices { - border: 1px solid #5897fb; - border-top-color: transparent; + border: 1px solid #5897fb; + border-top-color: transparent; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee)); - background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%); - background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); - background-image: linear-gradient(to bottom, #fff 0%, #eee 50%); + background-image: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0, #fff), + color-stop(0.5, #eee) + ); + background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%); + background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); + background-image: linear-gradient(to bottom, #fff 0%, #eee 50%); } .select2-dropdown-open .select2-choice .select2-arrow { - background: transparent; - border-left: none; - filter: none; + background: transparent; + border-left: none; + filter: none; } .select2-dropdown-open .select2-choice .select2-arrow b { - background-position: -18px 1px; + background-position: -18px 1px; } .select2-hidden-accessible { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } /* results */ .select2-results { - max-height: 200px; - padding: 0 0 0 4px; - margin: 4px 4px 4px 0; - position: relative; - overflow-x: hidden; - overflow-y: auto; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + max-height: 200px; + padding: 0 0 0 4px; + margin: 4px 4px 4px 0; + position: relative; + overflow-x: hidden; + overflow-y: auto; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .select2-results ul.select2-result-sub { - margin: 0; - padding-left: 0; + margin: 0; + padding-left: 0; } .select2-results li { - list-style: none; - display: list-item; - background-image: none; + list-style: none; + display: list-item; + background-image: none; } .select2-results li.select2-result-with-children > .select2-result-label { - font-weight: bold; + font-weight: bold; } .select2-results .select2-result-label { - padding: 3px 7px 4px; - margin: 0; - cursor: pointer; + padding: 3px 7px 4px; + margin: 0; + cursor: pointer; - min-height: 1em; + min-height: 1em; - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } -.select2-results-dept-1 .select2-result-label { padding-left: 20px } -.select2-results-dept-2 .select2-result-label { padding-left: 40px } -.select2-results-dept-3 .select2-result-label { padding-left: 60px } -.select2-results-dept-4 .select2-result-label { padding-left: 80px } -.select2-results-dept-5 .select2-result-label { padding-left: 100px } -.select2-results-dept-6 .select2-result-label { padding-left: 110px } -.select2-results-dept-7 .select2-result-label { padding-left: 120px } +.select2-results-dept-1 .select2-result-label { + padding-left: 20px; +} +.select2-results-dept-2 .select2-result-label { + padding-left: 40px; +} +.select2-results-dept-3 .select2-result-label { + padding-left: 60px; +} +.select2-results-dept-4 .select2-result-label { + padding-left: 80px; +} +.select2-results-dept-5 .select2-result-label { + padding-left: 100px; +} +.select2-results-dept-6 .select2-result-label { + padding-left: 110px; +} +.select2-results-dept-7 .select2-result-label { + padding-left: 120px; +} .select2-results .select2-highlighted { - background: #3875d7; - color: #fff; + background: #3875d7; + color: #fff; } .select2-results li em { - background: #feffde; - font-style: normal; + background: #feffde; + font-style: normal; } .select2-results .select2-highlighted em { - background: transparent; + background: transparent; } .select2-results .select2-highlighted ul { - background: #fff; - color: #000; + background: #fff; + color: #000; } - .select2-results .select2-no-results, .select2-results .select2-searching, .select2-results .select2-selection-limit { - background: #f4f4f4; - display: list-item; - padding-left: 5px; + background: #f4f4f4; + display: list-item; + padding-left: 5px; } /* disabled look for disabled choices in the results dropdown */ .select2-results .select2-disabled.select2-highlighted { - color: #666; - background: #f4f4f4; - display: list-item; - cursor: default; + color: #666; + background: #f4f4f4; + display: list-item; + cursor: default; } .select2-results .select2-disabled { background: #f4f4f4; @@ -535,56 +675,61 @@ disabled look for disabled choices in the results dropdown } .select2-results .select2-selected { - display: none; + display: none; } .select2-more-results.select2-active { - background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; + background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; } .select2-more-results { - background: #f4f4f4; - display: list-item; + background: #f4f4f4; + display: list-item; } /* disabled styles */ .select2-container.select2-container-disabled .select2-choice { - background-color: #f4f4f4; - background-image: none; - border: 1px solid #ddd; - cursor: default; + background-color: #f4f4f4; + background-image: none; + border: 1px solid #ddd; + cursor: default; } .select2-container.select2-container-disabled .select2-choice .select2-arrow { - background-color: #f4f4f4; - background-image: none; - border-left: 0; + background-color: #f4f4f4; + background-image: none; + border-left: 0; } .select2-container.select2-container-disabled .select2-choice abbr { - display: none; + display: none; } - /* multiselect */ .select2-container-multi .select2-choices { - height: auto !important; - height: 1%; - margin: 0; - padding: 0; - position: relative; + height: auto !important; + height: 1%; + margin: 0; + padding: 0; + position: relative; - border: 1px solid #aaa; - cursor: text; - overflow: hidden; + border: 1px solid #aaa; + cursor: text; + overflow: hidden; - background-color: #fff; - background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff)); - background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%); - background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%); - background-image: linear-gradient(to bottom, #eee 1%, #fff 15%); + background-color: #fff; + background-image: -webkit-gradient( + linear, + 0% 0%, + 0% 100%, + color-stop(1%, #eee), + color-stop(15%, #fff) + ); + background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%); + background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%); + background-image: linear-gradient(to bottom, #eee 1%, #fff 15%); } .select2-locked { @@ -592,197 +737,218 @@ disabled look for disabled choices in the results dropdown } .select2-container-multi .select2-choices { - min-height: 26px; + min-height: 26px; } .select2-container-multi.select2-container-active .select2-choices { - border: 1px solid #5897fb; - outline: none; + border: 1px solid #5897fb; + outline: none; - -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); - box-shadow: 0 0 5px rgba(0, 0, 0, .3); + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); } .select2-container-multi .select2-choices li { - float: left; - list-style: none; + float: left; + list-style: none; } -html[dir="rtl"] .select2-container-multi .select2-choices li -{ - float: right; +html[dir='rtl'] .select2-container-multi .select2-choices li { + float: right; } .select2-container-multi .select2-choices .select2-search-field { - margin: 0; - padding: 0; - white-space: nowrap; + margin: 0; + padding: 0; + white-space: nowrap; } .select2-container-multi .select2-choices .select2-search-field input { - padding: 5px; - margin: 1px 0; + padding: 5px; + margin: 1px 0; - font-family: sans-serif; - font-size: 100%; - color: #666; - outline: 0; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: transparent !important; + font-family: sans-serif; + font-size: 100%; + color: #666; + outline: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: transparent !important; } .select2-container-multi .select2-choices .select2-search-field input.select2-active { - background: #fff url('select2-spinner.gif') no-repeat 100% !important; + background: #fff url('select2-spinner.gif') no-repeat 100% !important; } .select2-default { - color: #999 !important; + color: #999 !important; } .select2-container-multi .select2-choices .select2-search-choice { - padding: 3px 5px 3px 18px; - margin: 3px 0 3px 5px; - position: relative; + padding: 3px 5px 3px 18px; + margin: 3px 0 3px 5px; + position: relative; - line-height: 13px; - color: #333; - cursor: default; - border: 1px solid #aaaaaa; + line-height: 13px; + color: #333; + cursor: default; + border: 1px solid #aaaaaa; - border-radius: 3px; + border-radius: 3px; - -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); - box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + -webkit-box-shadow: + 0 0 2px #fff inset, + 0 1px 0 rgba(0, 0, 0, 0.05); + box-shadow: + 0 0 2px #fff inset, + 0 1px 0 rgba(0, 0, 0, 0.05); - background-clip: padding-box; + background-clip: padding-box; - -webkit-touch-callout: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; - background-color: #e4e4e4; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0); - background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee)); - background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); - background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); - background-image: linear-gradient(to top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); -} -html[dir="rtl"] .select2-container-multi .select2-choices .select2-search-choice -{ - margin-left: 0; - margin-right: 5px; + background-color: #e4e4e4; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0); + background-image: -webkit-gradient( + linear, + 0% 0%, + 0% 100%, + color-stop(20%, #f4f4f4), + color-stop(50%, #f0f0f0), + color-stop(52%, #e8e8e8), + color-stop(100%, #eee) + ); + background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); + background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); + background-image: linear-gradient(to top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); +} +html[dir='rtl'] .select2-container-multi .select2-choices .select2-search-choice { + margin-left: 0; + margin-right: 5px; } .select2-container-multi .select2-choices .select2-search-choice .select2-chosen { - cursor: default; + cursor: default; } .select2-container-multi .select2-choices .select2-search-choice-focus { - background: #d4d4d4; + background: #d4d4d4; } .select2-search-choice-close { - display: block; - width: 12px; - height: 13px; - position: absolute; - right: 3px; - top: 4px; + display: block; + width: 12px; + height: 13px; + position: absolute; + right: 3px; + top: 4px; - font-size: 1px; - outline: none; - background: url('select2.png') right top no-repeat; + font-size: 1px; + outline: none; + background: url('select2.png') right top no-repeat; } -html[dir="rtl"] .select2-search-choice-close { - right: auto; - left: 3px; +html[dir='rtl'] .select2-search-choice-close { + right: auto; + left: 3px; } .select2-container-multi .select2-search-choice-close { - left: 3px; + left: 3px; } -.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { +.select2-container-multi + .select2-choices + .select2-search-choice + .select2-search-choice-close:hover { background-position: right -11px; } -.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { - background-position: right -11px; +.select2-container-multi + .select2-choices + .select2-search-choice-focus + .select2-search-choice-close { + background-position: right -11px; } /* disabled styles */ .select2-container-multi.select2-container-disabled .select2-choices { - background-color: #f4f4f4; - background-image: none; - border: 1px solid #ddd; - cursor: default; + background-color: #f4f4f4; + background-image: none; + border: 1px solid #ddd; + cursor: default; } .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { - padding: 3px 5px 3px 5px; - border: 1px solid #ddd; - background-image: none; - background-color: #f4f4f4; + padding: 3px 5px 3px 5px; + border: 1px solid #ddd; + background-image: none; + background-color: #f4f4f4; } -.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; - background: none; +.select2-container-multi.select2-container-disabled + .select2-choices + .select2-search-choice + .select2-search-choice-close { + display: none; + background: none; } /* end multiselect */ - .select2-result-selectable .select2-match, .select2-result-unselectable .select2-match { - text-decoration: underline; + text-decoration: underline; } -.select2-offscreen, .select2-offscreen:focus { - clip: rect(0 0 0 0) !important; - width: 1px !important; - height: 1px !important; - border: 0 !important; - margin: 0 !important; - padding: 0 !important; - overflow: hidden !important; - position: absolute !important; - outline: 0 !important; - left: 0px !important; - top: 0px !important; +.select2-offscreen, +.select2-offscreen:focus { + clip: rect(0 0 0 0) !important; + width: 1px !important; + height: 1px !important; + border: 0 !important; + margin: 0 !important; + padding: 0 !important; + overflow: hidden !important; + position: absolute !important; + outline: 0 !important; + left: 0px !important; + top: 0px !important; } .select2-display-none { - display: none; + display: none; } .select2-measure-scrollbar { - position: absolute; - top: -10000px; - left: -10000px; - width: 100px; - height: 100px; - overflow: scroll; + position: absolute; + top: -10000px; + left: -10000px; + width: 100px; + height: 100px; + overflow: scroll; } /* Retina-ize icons */ -@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) { - .select2-search input, - .select2-search-choice-close, - .select2-container .select2-choice abbr, - .select2-container .select2-choice .select2-arrow b { - background-image: url('select2x2.png') !important; - background-repeat: no-repeat !important; - background-size: 60px 40px !important; - } +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), + only screen and (min-resolution: 2dppx) { + .select2-search input, + .select2-search-choice-close, + .select2-container .select2-choice abbr, + .select2-container .select2-choice .select2-arrow b { + background-image: url('select2x2.png') !important; + background-repeat: no-repeat !important; + background-size: 60px 40px !important; + } - .select2-search input { - background-position: 100% -21px !important; - } + .select2-search input { + background-position: 100% -21px !important; + } } .task-checklist-edit > .checklist-form li:after, .task-filter:after, .filters:after, .filters .filters-controls:after { - content: ""; + content: ''; display: table; clear: both; } @@ -2232,7 +2398,11 @@ html[dir="rtl"] .select2-search-choice-close { font-weight: bold; letter-spacing: 0.0618em; color: #fff; - text-shadow: -1px -1px 1px #333, 1px -1px 1px #333, -1px 1px 1px #333, 1px 1px 1px #333; + text-shadow: + -1px -1px 1px #333, + 1px -1px 1px #333, + -1px 1px 1px #333, + 1px 1px 1px #333; } .herobox .avatar-level > a, .herobox .avatar-name > a, @@ -2270,8 +2440,8 @@ html[dir="rtl"] .select2-search-choice-close { left: 2%; width: 96%; height: 96%; - -webkit-box-shadow: 0 0 0 30px rgba(0,0,0,0.63); - box-shadow: 0 0 0 30px rgba(0,0,0,0.63); + -webkit-box-shadow: 0 0 0 30px rgba(0, 0, 0, 0.63); + box-shadow: 0 0 0 30px rgba(0, 0, 0, 0.63); } .toolbar-mobile > div h4:before, .toolbar-nav .toolbar-button-dropdown > div h4:before, @@ -2439,7 +2609,7 @@ html[dir="rtl"] .select2-search-choice-close { } @media screen and (max-width: 767px) { .toolbar-controls, -.toolbar-controls { + .toolbar-controls { width: 96%; position: fixed; bottom: 2%; @@ -2449,7 +2619,7 @@ html[dir="rtl"] .select2-search-choice-close { } @media screen and (min-width: 768px) { .toolbar-controls, -.toolbar-controls { + .toolbar-controls { display: none; } } @@ -2460,8 +2630,8 @@ html[dir="rtl"] .select2-search-choice-close { @media screen and (min-width: 768px) { .options-menu, .options-submenu, -.options-menu, -.options-submenu { + .options-menu, + .options-submenu { padding: 1em 1em 0em 1em; } } @@ -2550,26 +2720,66 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #763225 !important; outline: none; } -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li > input + button:focus, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li + > input + + button:focus, .task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li > input + button:focus, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li > input + button:focus, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li textarea + button:focus, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li + textarea + + button:focus, .task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li textarea + button:focus, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li textarea + button:focus, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li textarea + button:focus { +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li + textarea + + button:focus { border-color: #c65e4a !important; background-color: #e0a79c !important; outline: none; } -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li > input + button:active, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li + > input + + button:active, .task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li > input + button:active, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li > input + button:active, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li textarea + button:active, -.task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li textarea + button:active, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li + > input + + button:active, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li + textarea + + button:active, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-attributes + li + textarea + + button:active, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #d68c7d !important; } .task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li > a:nth-of-type(2), @@ -2594,14 +2804,54 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #e6b8af; border-color: #d68c7d; } -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-days + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #d68c7d; } @@ -2655,26 +2905,77 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #e2aea3 !important; border-color: #9a4230 !important; } -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li.active.filters-tags a, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li.active.filters-tags + a, .task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li.active.filters-tags a, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li.active.filters-tags button, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-attributes + li.active.filters-tags + button, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #c65e4a !important; border-color: #e6b8af !important; color: #fff !important; } -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li.active.filters-tags a span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-attributes + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-worst:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-worst:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-worst:not(.completed) .priority-multiplier li button.active, @@ -2695,7 +2996,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-worst:not(.completed) .plusminus .task-checker label:after { border: 1px solid #c96652 !important; } -.task-column:not(.rewards) .color-worst:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-worst:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #b94f3a !important; box-shadow: inset 0 0 0 1px #b94f3a !important; background-color: #e1aaa0 !important; @@ -2745,17 +3051,37 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-worst:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #c35440 !important; background-color: #db9a8e !important; outline: none; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-worst:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #d28071 !important; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close > a:nth-of-type(2), @@ -2775,9 +3101,19 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #d28071; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-worst:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #d28071; } @@ -2814,17 +3150,35 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #923e2e !important; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close.active.filters-tags a, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li.active.filters-tags a, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li.active.filters-tags + a, .task-column:not(.rewards) .color-worst:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #c35440 !important; border-color: #e1aaa0 !important; color: #fff !important; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li.active.filters-tags a span, +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-worst:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-worst:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-worst:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-worst:not(.completed) .save-close button:focus, @@ -2848,7 +3202,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-worst:not(.completed) .task-actions a:focus { background-color: #b94f3a; } -.task-column:not(.rewards) .color-worst:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-worst:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-worst:not(.completed) input.habit:focus + a { background-color: #b94f3a; } @@ -2947,26 +3304,66 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #8d1e1e !important; outline: none; } -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li > input + button:focus, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li + > input + + button:focus, .task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li > input + button:focus, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li > input + button:focus, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li textarea + button:focus, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li + textarea + + button:focus, .task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li textarea + button:focus, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li textarea + button:focus, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li textarea + button:focus { +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li + textarea + + button:focus { border-color: #da5353 !important; background-color: #efb5b5 !important; outline: none; } -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li > input + button:active, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li + > input + + button:active, .task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li > input + button:active, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li > input + button:active, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li textarea + button:active, -.task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li textarea + button:active, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li + > input + + button:active, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li + textarea + + button:active, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-attributes + li + textarea + + button:active, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #e79090 !important; } .task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li > a:nth-of-type(2), @@ -2991,14 +3388,54 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #f4cccc; border-color: #e79090; } -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-days + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #e79090; } @@ -3052,26 +3489,77 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #f1bebe !important; border-color: #b82828 !important; } -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li.active.filters-tags a, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li.active.filters-tags + a, .task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li.active.filters-tags a, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li.active.filters-tags button, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-attributes + li.active.filters-tags + button, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #da5353 !important; border-color: #f4cccc !important; color: #fff !important; } -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li.active.filters-tags a span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-attributes + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-worse:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-worse:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-worse:not(.completed) .priority-multiplier li button.active, @@ -3092,7 +3580,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-worse:not(.completed) .plusminus .task-checker label:after { border: 1px solid #dc5d5d !important; } -.task-column:not(.rewards) .color-worse:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-worse:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #d43939 !important; box-shadow: inset 0 0 0 1px #d43939 !important; background-color: #f0baba !important; @@ -3142,17 +3635,37 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-worse:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #d74747 !important; background-color: #eba4a4 !important; outline: none; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-worse:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #e48181 !important; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close > a:nth-of-type(2), @@ -3172,9 +3685,19 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #e48181; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-worse:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #e48181; } @@ -3211,17 +3734,35 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #af2626 !important; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close.active.filters-tags a, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li.active.filters-tags a, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li.active.filters-tags + a, .task-column:not(.rewards) .color-worse:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #d74747 !important; border-color: #f0baba !important; color: #fff !important; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li.active.filters-tags a span, +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-worse:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-worse:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-worse:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-worse:not(.completed) .save-close button:focus, @@ -3245,7 +3786,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-worse:not(.completed) .task-actions a:focus { background-color: #d43939; } -.task-column:not(.rewards) .color-worse:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-worse:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-worse:not(.completed) input.habit:focus + a { background-color: #d43939; } @@ -3344,11 +3888,21 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #a5590a !important; outline: none; } -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li > input + button:focus, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li + > input + + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li > input + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li > input + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li textarea + button:focus, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li + textarea + + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li textarea + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li textarea + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li textarea + button:focus { @@ -3356,14 +3910,29 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #fad7b2 !important; outline: none; } -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li > input + button:active, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li + > input + + button:active, .task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li > input + button:active, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li > input + button:active, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li textarea + button:active, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li + textarea + + button:active, .task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li textarea + button:active, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #f8c187 !important; } .task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li > a:nth-of-type(2), @@ -3388,14 +3957,49 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #fce5cd; border-color: #f8c187; } -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #f8c187; } @@ -3453,22 +4057,69 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li.active.filters-tags a, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li.active.filters-tags button, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-attributes + li.active.filters-tags + button, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #f49b40 !important; border-color: #fce5cd !important; color: #fff !important; } -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li.active.filters-tags a span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-attributes + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-bad:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-bad:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-bad:not(.completed) .priority-multiplier li button.active, @@ -3489,7 +4140,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-bad:not(.completed) .plusminus .task-checker label:after { border: 1px solid #f4a24c !important; } -.task-column:not(.rewards) .color-bad:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-bad:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #f28b21 !important; box-shadow: inset 0 0 0 1px #f28b21 !important; background-color: #fbdab7 !important; @@ -3539,17 +4195,37 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-bad:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-bad:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #f49530 !important; background-color: #facd9e !important; outline: none; } .task-column:not(.rewards) .color-bad:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-bad:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #f7b874 !important; } .task-column:not(.rewards) .color-bad:not(.completed) .save-close > a:nth-of-type(2), @@ -3569,9 +4245,19 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #f7b874; } .task-column:not(.rewards) .color-bad:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-bad:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #f7b874; } @@ -3610,15 +4296,29 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-bad:not(.completed) .save-close.active.filters-tags a, .task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li.active.filters-tags a, .task-column:not(.rewards) .color-bad:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #f49530 !important; border-color: #fbdab7 !important; color: #fff !important; } .task-column:not(.rewards) .color-bad:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li.active.filters-tags a span, +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-bad:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-bad:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-bad:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-bad:not(.completed) .save-close button:focus, @@ -3642,7 +4342,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-bad:not(.completed) .task-actions a:focus { background-color: #f28b21; } -.task-column:not(.rewards) .color-bad:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-bad:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-bad:not(.completed) input.habit:focus + a { background-color: #f28b21; } @@ -3741,29 +4444,93 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #af8300 !important; outline: none; } -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li > input + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li > input + button:focus, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + > input + + button:focus, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li + > input + + button:focus, .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li > input + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li textarea + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li textarea + button:focus, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li + > input + + button:focus, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + textarea + + button:focus, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li + textarea + + button:focus, .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li textarea + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li textarea + button:focus { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li + textarea + + button:focus { border-color: #ffcc35 !important; background-color: #ffebb0 !important; outline: none; } -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li > input + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li > input + button:active, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + > input + + button:active, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li + > input + + button:active, .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li > input + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li textarea + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li textarea + button:active, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li + > input + + button:active, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + textarea + + button:active, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li + textarea + + button:active, .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #ffdf82 !important; } -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li > a:nth-of-type(2), +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + > a:nth-of-type(2), .task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li > a:nth-of-type(2), .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li > a:nth-of-type(2), .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li > a:nth-of-type(2) { @@ -3785,14 +4552,54 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #fff2cc; border-color: #ffdf82; } -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li > div > ul:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-days + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #ffdf82; } @@ -3846,26 +4653,90 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #ffeeba !important; border-color: #e6ab00 !important; } -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li.active.filters-tags a, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li.active.filters-tags + a, .task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li.active.filters-tags a, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li.active.filters-tags button, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li.active.filters-tags + a, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-days + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #ffcc35 !important; border-color: #fff2cc !important; color: #fff !important; } -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li.active.filters-tags a span, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-neutral:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-days + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-neutral:not(.completed) .priority-multiplier li button.active, @@ -3886,7 +4757,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-neutral:not(.completed) .plusminus .task-checker label:after { border: 1px solid #ffcf42 !important; } -.task-column:not(.rewards) .color-neutral:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #ffc314 !important; box-shadow: inset 0 0 0 1px #ffc314 !important; background-color: #ffecb5 !important; @@ -3936,21 +4812,45 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-neutral:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #ffc726 !important; background-color: #ffe59a !important; outline: none; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-neutral:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #ffda6e !important; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close > a:nth-of-type(2), -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li > a:nth-of-type(2) { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + > a:nth-of-type(2) { border-left: 1px solid #ffe8a4 !important; } @media screen and (min-width: 768px) { @@ -3966,9 +4866,23 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #ffda6e; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li > div > ul:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .save-close + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #ffda6e; } @@ -4005,17 +4919,39 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #daa200 !important; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close.active.filters-tags a, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li.active.filters-tags a, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li.active.filters-tags + a, .task-column:not(.rewards) .color-neutral:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #ffc726 !important; border-color: #ffecb5 !important; color: #fff !important; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li.active.filters-tags a span, -.task-column:not(.rewards) .color-neutral:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-neutral:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .save-close.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-neutral:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-neutral:not(.completed) .save-close button:focus, @@ -4039,7 +4975,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-neutral:not(.completed) .task-actions a:focus { background-color: #ffc314; } -.task-column:not(.rewards) .color-neutral:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-neutral:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-neutral:not(.completed) input.habit:focus + a { background-color: #ffc314; } @@ -4138,26 +5077,56 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #477337 !important; outline: none; } -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li > input + button:focus, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li + > input + + button:focus, .task-column:not(.rewards) .color-good:not(.completed) .task-attributes li > input + button:focus, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li > input + button:focus, .task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li textarea + button:focus, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li + textarea + + button:focus, .task-column:not(.rewards) .color-good:not(.completed) .task-attributes li textarea + button:focus, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li textarea + button:focus, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li textarea + button:focus { +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li + textarea + + button:focus { border-color: #84bb70 !important; background-color: #c9e1c0 !important; outline: none; } -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li > input + button:active, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li + > input + + button:active, .task-column:not(.rewards) .color-good:not(.completed) .task-attributes li > input + button:active, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li > input + button:active, .task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li textarea + button:active, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li + textarea + + button:active, .task-column:not(.rewards) .color-good:not(.completed) .task-attributes li textarea + button:active, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #afd3a2 !important; } .task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li > a:nth-of-type(2), @@ -4182,14 +5151,54 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #d9ead3; border-color: #afd3a2; } -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .task-attributes li > div > ul:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-days + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #afd3a2; } @@ -4243,26 +5252,77 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #cfe5c7 !important; border-color: #5c9748 !important; } -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li.active.filters-tags a, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li.active.filters-tags + a, .task-column:not(.rewards) .color-good:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li.active.filters-tags a, .task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-good:not(.completed) .task-attributes li.active.filters-tags button, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-attributes + li.active.filters-tags + button, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #84bb70 !important; border-color: #d9ead3 !important; color: #fff !important; } -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-good:not(.completed) .task-attributes li.active.filters-tags a span, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-attributes + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-good:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-good:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-good:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-good:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-good:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-good:not(.completed) .priority-multiplier li button.active, @@ -4283,7 +5343,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-good:not(.completed) .plusminus .task-checker label:after { border: 1px solid #8bbf79 !important; } -.task-column:not(.rewards) .color-good:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-good:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #71b05b !important; box-shadow: inset 0 0 0 1px #71b05b !important; background-color: #cce3c4 !important; @@ -4333,17 +5398,37 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-good:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-good:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #7bb666 !important; background-color: #bddbb2 !important; outline: none; } .task-column:not(.rewards) .color-good:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-good:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #a4cd96 !important; } .task-column:not(.rewards) .color-good:not(.completed) .save-close > a:nth-of-type(2), @@ -4363,9 +5448,19 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #a4cd96; } .task-column:not(.rewards) .color-good:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-good:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #a4cd96; } @@ -4402,17 +5497,35 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #588f44 !important; } .task-column:not(.rewards) .color-good:not(.completed) .save-close.active.filters-tags a, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li.active.filters-tags a, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li.active.filters-tags + a, .task-column:not(.rewards) .color-good:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #7bb666 !important; border-color: #cce3c4 !important; color: #fff !important; } .task-column:not(.rewards) .color-good:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li.active.filters-tags a span, +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-good:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-good:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-good:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-good:not(.completed) .save-close button:focus, @@ -4436,7 +5549,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-good:not(.completed) .task-actions a:focus { background-color: #71b05b; } -.task-column:not(.rewards) .color-good:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-good:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-good:not(.completed) input.habit:focus + a { background-color: #71b05b; } @@ -4535,26 +5651,81 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #3e6168 !important; outline: none; } -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li > input + button:focus, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li + > input + + button:focus, .task-column:not(.rewards) .color-better:not(.completed) .task-attributes li > input + button:focus, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li > input + button:focus, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li textarea + button:focus, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li textarea + button:focus, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li + > input + + button:focus, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li + textarea + + button:focus, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li + textarea + + button:focus, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li textarea + button:focus, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li textarea + button:focus { +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li + textarea + + button:focus { border-color: #77a5ae !important; background-color: #bfd5d9 !important; outline: none; } -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li > input + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li > input + button:active, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li + > input + + button:active, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li + > input + + button:active, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li > input + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li textarea + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li textarea + button:active, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li + > input + + button:active, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li + textarea + + button:active, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li + textarea + + button:active, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #a4c3c9 !important; } .task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li > a:nth-of-type(2), @@ -4579,14 +5750,54 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #d0e0e3; border-color: #a4c3c9; } -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li > div > ul:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-days + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #a4c3c9; } @@ -4640,26 +5851,77 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #c6d9dd !important; border-color: #518088 !important; } -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li.active.filters-tags a, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li.active.filters-tags + a, .task-column:not(.rewards) .color-better:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li.active.filters-tags a, .task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li.active.filters-tags button, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li.active.filters-tags + button, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #77a5ae !important; border-color: #d0e0e3 !important; color: #fff !important; } -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li.active.filters-tags a span, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-better:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-better:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-better:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-better:not(.completed) .priority-multiplier li button.active, @@ -4680,7 +5942,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-better:not(.completed) .plusminus .task-checker label:after { border: 1px solid #7eaab2 !important; } -.task-column:not(.rewards) .color-better:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-better:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #6398a2 !important; box-shadow: inset 0 0 0 1px #6398a2 !important; background-color: #c2d7db !important; @@ -4730,21 +5997,45 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-better:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-better:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #6d9fa9 !important; background-color: #b2ccd2 !important; outline: none; } .task-column:not(.rewards) .color-better:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-better:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #98bbc2 !important; } .task-column:not(.rewards) .color-better:not(.completed) .save-close > a:nth-of-type(2), -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li > a:nth-of-type(2) { +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + > a:nth-of-type(2) { border-left: 1px solid #b8d0d5 !important; } @media screen and (min-width: 768px) { @@ -4760,9 +6051,19 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #98bbc2; } .task-column:not(.rewards) .color-better:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-better:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #98bbc2; } @@ -4799,17 +6100,39 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #4d7982 !important; } .task-column:not(.rewards) .color-better:not(.completed) .save-close.active.filters-tags a, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li.active.filters-tags a, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li.active.filters-tags + a, .task-column:not(.rewards) .color-better:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #6d9fa9 !important; border-color: #c2d7db !important; color: #fff !important; } .task-column:not(.rewards) .color-better:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li.active.filters-tags a span, -.task-column:not(.rewards) .color-better:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-better:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .save-close.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-better:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-better:not(.completed) .save-close button:focus, @@ -4833,7 +6156,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-better:not(.completed) .task-actions a:focus { background-color: #6398a2; } -.task-column:not(.rewards) .color-better:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-better:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-better:not(.completed) input.habit:focus + a { background-color: #6398a2; } @@ -4932,26 +6258,56 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #144398 !important; outline: none; } -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li > input + button:focus, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li + > input + + button:focus, .task-column:not(.rewards) .color-best:not(.completed) .task-attributes li > input + button:focus, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li > input + button:focus, .task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li > input + button:focus, -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li textarea + button:focus, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li + textarea + + button:focus, .task-column:not(.rewards) .color-best:not(.completed) .task-attributes li textarea + button:focus, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li textarea + button:focus, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li textarea + button:focus { +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li + textarea + + button:focus { border-color: #4781e7 !important; background-color: #b0c9f5 !important; outline: none; } -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li > input + button:active, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li + > input + + button:active, .task-column:not(.rewards) .color-best:not(.completed) .task-attributes li > input + button:active, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li > input + button:active, .task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li > input + button:active, -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li textarea + button:active, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li + textarea + + button:active, .task-column:not(.rewards) .color-best:not(.completed) .task-attributes li textarea + button:active, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li textarea + button:active, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li textarea + button:active { +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li + textarea + + button:active { background-color: #89aef0 !important; } .task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li > a:nth-of-type(2), @@ -4976,14 +6332,54 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #c9daf8; border-color: #89aef0; } -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li > div > ul:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .task-attributes li > div > ul:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-days li > div > ul:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li > div > ul:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li > div > div:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .task-attributes li > div > div:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-days li > div > div:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li > div > div:first-child:before { +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-attributes + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-days + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li + > div + > ul:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-attributes + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-days + li + > div + > div:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li + > div + > div:first-child:before { background-color: #fff; border-color: #89aef0; } @@ -5037,26 +6433,77 @@ html[dir="rtl"] .select2-search-choice-close { background-color: #bad0f6 !important; border-color: #1a58c7 !important; } -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li.active.filters-tags a, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li.active.filters-tags + a, .task-column:not(.rewards) .color-best:not(.completed) .task-attributes li.active.filters-tags a, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li.active.filters-tags a, .task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li.active.filters-tags a, -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li.active.filters-tags button, -.task-column:not(.rewards) .color-best:not(.completed) .task-attributes li.active.filters-tags button, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li.active.filters-tags + button, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-attributes + li.active.filters-tags + button, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li.active.filters-tags button, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li.active.filters-tags button { +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li.active.filters-tags + button { background-color: #4781e7 !important; border-color: #c9daf8 !important; color: #fff !important; } -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li.active.filters-tags a span, -.task-column:not(.rewards) .color-best:not(.completed) .task-attributes li.active.filters-tags a span, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-attributes + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-best:not(.completed) .repeat-days li.active.filters-tags a span, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li.active.filters-tags a span, -.task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li.active.filters-tags button span, -.task-column:not(.rewards) .color-best:not(.completed) .task-attributes li.active.filters-tags button span, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-days li.active.filters-tags button span, -.task-column:not(.rewards) .color-best:not(.completed) .repeat-frequency li.active.filters-tags button span { +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li.active.filters-tags + a + span, +.task-column:not(.rewards) + .color-best:not(.completed) + .priority-multiplier + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-attributes + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-days + li.active.filters-tags + button + span, +.task-column:not(.rewards) + .color-best:not(.completed) + .repeat-frequency + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-best:not(.completed) .priority-multiplier li button.active, @@ -5077,7 +6524,12 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-best:not(.completed) .plusminus .task-checker label:after { border: 1px solid #5288e9 !important; } -.task-column:not(.rewards) .color-best:not(.completed) .plusminus .task-checker input[type=checkbox]:checked + label:after { +.task-column:not(.rewards) + .color-best:not(.completed) + .plusminus + .task-checker + input[type='checkbox']:checked + + label:after { -webkit-box-shadow: inset 0 0 0 1px #2a6de3 !important; box-shadow: inset 0 0 0 1px #2a6de3 !important; background-color: #b5ccf5 !important; @@ -5127,17 +6579,37 @@ html[dir="rtl"] .select2-search-choice-close { outline: none; } .task-column:not(.rewards) .color-best:not(.completed) .save-close > input + button:focus, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li > input + button:focus, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li + > input + + button:focus, .task-column:not(.rewards) .color-best:not(.completed) .save-close textarea + button:focus, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li textarea + button:focus { +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li + textarea + + button:focus { border-color: #3a77e4 !important; background-color: #9ebcf2 !important; outline: none; } .task-column:not(.rewards) .color-best:not(.completed) .save-close > input + button:active, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li > input + button:active, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li + > input + + button:active, .task-column:not(.rewards) .color-best:not(.completed) .save-close textarea + button:active, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li textarea + button:active { +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li + textarea + + button:active { background-color: #78a2ed !important; } .task-column:not(.rewards) .color-best:not(.completed) .save-close > a:nth-of-type(2), @@ -5157,9 +6629,19 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #78a2ed; } .task-column:not(.rewards) .color-best:not(.completed) .save-close > div > ul:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li > div > ul:first-child:before, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li + > div + > ul:first-child:before, .task-column:not(.rewards) .color-best:not(.completed) .save-close > div > div:first-child:before, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li > div > div:first-child:before { +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li + > div + > div:first-child:before { background-color: #fff; border-color: #78a2ed; } @@ -5196,17 +6678,35 @@ html[dir="rtl"] .select2-search-choice-close { border-color: #1954bc !important; } .task-column:not(.rewards) .color-best:not(.completed) .save-close.active.filters-tags a, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li.active.filters-tags a, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li.active.filters-tags + a, .task-column:not(.rewards) .color-best:not(.completed) .save-close.active.filters-tags button, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li.active.filters-tags button { +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li.active.filters-tags + button { background-color: #3a77e4 !important; border-color: #b5ccf5 !important; color: #fff !important; } .task-column:not(.rewards) .color-best:not(.completed) .save-close.active.filters-tags a span, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li.active.filters-tags a span, +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li.active.filters-tags + a + span, .task-column:not(.rewards) .color-best:not(.completed) .save-close.active.filters-tags button span, -.task-column:not(.rewards) .color-best:not(.completed) .task-checklist-edit li.active.filters-tags button span { +.task-column:not(.rewards) + .color-best:not(.completed) + .task-checklist-edit + li.active.filters-tags + button + span { color: #fff !important; } .task-column:not(.rewards) .color-best:not(.completed) .save-close button:focus, @@ -5230,7 +6730,10 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:not(.rewards) .color-best:not(.completed) .task-actions a:focus { background-color: #2a6de3; } -.task-column:not(.rewards) .color-best:not(.completed) input[type=checkbox].task-input:focus + label, +.task-column:not(.rewards) + .color-best:not(.completed) + input[type='checkbox'].task-input:focus + + label, .task-column:not(.rewards) .color-best:not(.completed) input.habit:focus + a { background-color: #2a6de3; } @@ -5260,7 +6763,7 @@ html[dir="rtl"] .select2-search-choice-close { } .completed .task-text .habitica-emoji { opacity: 0.39; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=39)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=39)'; filter: alpha(opacity=39); } .completed .priority-multiplier li > a, @@ -5480,7 +6983,7 @@ html[dir="rtl"] .select2-search-choice-close { .completed .plusminus .task-checker label:after { border: 1px solid #989898 !important; } -.completed .plusminus .task-checker input[type=checkbox]:checked + label:after { +.completed .plusminus .task-checker input[type='checkbox']:checked + label:after { -webkit-box-shadow: inset 0 0 0 1px #828282 !important; box-shadow: inset 0 0 0 1px #828282 !important; background-color: #cecece !important; @@ -5627,7 +7130,7 @@ html[dir="rtl"] .select2-search-choice-close { .completed .task-action-btn:focus { background-color: #828282; } -.completed input[type=checkbox]:focus + label { +.completed input[type='checkbox']:focus + label { background-color: #828282; } .completed .task-options { @@ -5668,7 +7171,7 @@ html[dir="rtl"] .select2-search-choice-close { .task-column:after { clear: both; display: block; - content: ""; + content: ''; } .task-column h2 { color: #4c666e; @@ -5840,8 +7343,8 @@ html[dir="rtl"] .select2-search-choice-close { .task label { font-weight: 400; } -.task input[type="text"], -.task input[type="number"], +.task input[type='text'], +.task input[type='number'], .task textarea.option-content { border: 1px solid #aaa; -webkit-border-radius: 0.382em; @@ -5865,14 +7368,18 @@ html[dir="rtl"] .select2-search-choice-close { margin-left: 20px; } .task.ui-sortable-helper { - -webkit-box-shadow: 0 0 3px rgba(0,0,0,0.15), 0 0 5px rgba(0,0,0,0.1); - box-shadow: 0 0 3px rgba(0,0,0,0.15), 0 0 5px rgba(0,0,0,0.1); + -webkit-box-shadow: + 0 0 3px rgba(0, 0, 0, 0.15), + 0 0 5px rgba(0, 0, 0, 0.1); + box-shadow: + 0 0 3px rgba(0, 0, 0, 0.15), + 0 0 5px rgba(0, 0, 0, 0.1); -webkit-transform: scale(1.05); -moz-transform: scale(1.05); -o-transform: scale(1.05); -ms-transform: scale(1.05); transform: scale(1.05); - outline: 1px solid rgba(0,0,0,0.2); + outline: 1px solid rgba(0, 0, 0, 0.2); } .task-controls { display: inline-block; @@ -5898,7 +7405,7 @@ html[dir="rtl"] .select2-search-choice-close { text-align: center; color: #222; vertical-align: top; - border-right: 1px solid rgba(0,0,0,0.25); + border-right: 1px solid rgba(0, 0, 0, 0.25); } .task-action-btn:last-child { border: 0; @@ -5908,14 +7415,14 @@ html[dir="rtl"] .select2-search-choice-close { color: #222; text-decoration: none; } -.task-checker input[type=checkbox], -.task-checker input[type=checkbox]:focus { +.task-checker input[type='checkbox'], +.task-checker input[type='checkbox']:focus { position: absolute; margin: 0; padding: 0; height: 10px; opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); width: 10px; } @@ -5941,10 +7448,10 @@ html[dir="rtl"] .select2-search-choice-close { width: 2em; line-height: 1.5; } -.plusminus .task-checker label[for$="plus"]:after { +.plusminus .task-checker label[for$='plus']:after { content: '+'; } -.plusminus .task-checker label[for$="minus"]:after { +.plusminus .task-checker label[for$='minus']:after { content: '−'; } .action-yesno { @@ -5961,7 +7468,7 @@ html[dir="rtl"] .select2-search-choice-close { text-align: center; color: #000; opacity: 0.2; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=20)'; filter: alpha(opacity=20); } .action-yesno label:after { @@ -5978,15 +7485,15 @@ html[dir="rtl"] .select2-search-choice-close { .action-yesno label:focus:before { content: ''; } -.action-yesno input[type=checkbox]:focus + label { +.action-yesno input[type='checkbox']:focus + label { opacity: 1 !important; -ms-filter: none; filter: none; border: none; } .action-yesno label:hover:after { - content: "\E013"; - font-family: "Glyphicons Halflings"; + content: '\E013'; + font-family: 'Glyphicons Halflings'; border: none; margin: 0; line-height: 1.714285714em; @@ -5994,17 +7501,17 @@ html[dir="rtl"] .select2-search-choice-close { width: 1.714285714em; text-align: center; opacity: 0.5 !important; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)" !important; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)' !important; filter: alpha(opacity=50) !important; } .action-yesno label:active:after { opacity: 0.75 !important; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)" !important; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=75)' !important; filter: alpha(opacity=75) !important; } -.action-yesno input[type=checkbox]:checked + label:after { - content: "\E013"; - font-family: "Glyphicons Halflings"; +.action-yesno input[type='checkbox']:checked + label:after { + content: '\E013'; + font-family: 'Glyphicons Halflings'; border: none; margin: 0; line-height: 1.714285714em; @@ -6012,7 +7519,7 @@ html[dir="rtl"] .select2-search-choice-close { width: 1.714285714em; text-align: center; opacity: 0.75; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=75)'; filter: alpha(opacity=75); } .task-meta-controls { @@ -6020,7 +7527,7 @@ html[dir="rtl"] .select2-search-choice-close { margin: 0.75em 0.5em 0 0.5em; height: 1em; opacity: 0.25; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=25)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=25)'; filter: alpha(opacity=25); } .task-meta-controls a { @@ -6064,17 +7571,17 @@ form { color: #333; position: relative; }*/ -[class$="-options"] .option-group { +[class$='-options'] .option-group { padding: 0 0 1em; margin-bottom: 1em; margin-top: 1em; } -[class$="-options"] button.advanced-options-toggle { +[class$='-options'] button.advanced-options-toggle { display: block; width: 100%; background: none; } -[class$="-options"] .option-title { +[class$='-options'] .option-title { font-size: 1em; margin: 0.5em 0 0.5em; line-height: 1; @@ -6083,17 +7590,17 @@ form { font-weight: bold; text-align: center; } -[class$="-options"] .option-title.mega { +[class$='-options'] .option-title.mega { cursor: pointer; } -[class$="-options"] .option-title.mega:after { - font-family: "Glyphicons Halflings"; +[class$='-options'] .option-title.mega:after { + font-family: 'Glyphicons Halflings'; font-size: 0.75em; - content: "\E114"; + content: '\E114'; padding-left: 0.75em; } -[class$="-options"] .option-title.mega.active:after { - content: "\E113"; +[class$='-options'] .option-title.mega.active:after { + content: '\E113'; } .option-content { height: 2.5em; @@ -6135,12 +7642,12 @@ textarea.option-content { border: 0; font-size: 1.15em; font-weight: 300; - outline: 1px solid rgba(0,0,0,0.2); + outline: 1px solid rgba(0, 0, 0, 0.2); outline-offset: -1px; margin: 0 0 0 3px; text-align: inherit; opacity: 0.5; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; filter: alpha(opacity=50); width: auto; padding: 0 0.5em; @@ -6171,7 +7678,7 @@ textarea.option-content { } .tile.flush { margin-left: 0; - border: 1px solid rgba(0,0,0,0.2); + border: 1px solid rgba(0, 0, 0, 0.2); outline: 0; line-height: 2em; } @@ -6266,7 +7773,7 @@ textarea.option-content { } .task-checklist-edit > .checklist-form input { width: 70%; -/* Add interaction cues on hover and focus */ + /* Add interaction cues on hover and focus */ } .task-checklist-edit > .checklist-form input:hover, .task-checklist-edit > .checklist-form input:focus { @@ -6279,7 +7786,7 @@ textarea.option-content { } .task-checklist-edit > .checklist-form .checklist-icon { opacity: 0.25; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=25)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=25)'; filter: alpha(opacity=25); text-align: center; line-height: 1.5; @@ -6434,7 +7941,7 @@ textarea.option-content { -webkit-box-shadow: none; box-shadow: none; opacity: 0.65; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=65)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=65)'; filter: alpha(opacity=65); } .rewards .btn-buy span { @@ -6463,7 +7970,7 @@ textarea.option-content { margin: 15px auto; border: 1px solid #222; opacity: 0.2 !important; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)" !important; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=20)' !important; filter: alpha(opacity=20) !important; } .locked-task .action-yesno label:focus, @@ -6533,7 +8040,7 @@ textarea.option-content { top: 4px; left: 4px; opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); -webkit-transition: opacity 0.2s ease-out; -moz-transition: opacity 0.2s ease-out; @@ -6589,7 +8096,7 @@ textarea.option-content { left: 4px; z-index: 2; opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } .herobox:hover .addthis_native_toolbox { @@ -6716,7 +8223,7 @@ menu { } .btn-buy input:focus { opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } input:focus + a.btn-buy { @@ -6730,7 +8237,7 @@ input:focus + a.btn-buy { .rewards { margin-bottom: 1.5em; padding-bottom: 1.5em; - border-bottom: 1px solid rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .reward-item { background: #fff; @@ -6755,8 +8262,8 @@ input:focus + a.btn-buy { padding-left: 0.25em; background-color: #d0e0e3; cursor: pointer; - -webkit-box-shadow: inset -1px -1px 0 rgba(0,0,0,0.1); - box-shadow: inset -1px -1px 0 rgba(0,0,0,0.1); + -webkit-box-shadow: inset -1px -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: inset -1px -1px 0 rgba(0, 0, 0, 0.1); } .btn-reroll:hover, .btn-reroll:focus { @@ -6781,7 +8288,7 @@ input:focus + a.btn-buy { } .gem-wallet .add-gems-btn { opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } .gem-wallet:hover .add-gems-btn, @@ -6890,9 +8397,9 @@ menu.pets .customize-menu .progress { .mount-not-owned { width: 81px; height: 99px; -/* Would use css3 filters and just display the original pet image with a black hue, + /* Would use css3 filters and just display the original pet image with a black hue, but doesn't seem to work in Firefox or Opera */ -/*filter: brightness(0%) + /*filter: brightness(0%) -webkit-filter: brightness(0%) -moz-filter: brightness(0%) -o-filter: brightness(0%) @@ -6904,7 +8411,7 @@ menu.pets .customize-menu .progress { } .pet-evolved { opacity: 0.1; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=10)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=10)'; filter: alpha(opacity=10); } .selectableInventory { @@ -6925,7 +8432,7 @@ menu.pets .customize-menu .progress { height: 0; z-index: 1010; } -.new-stuff> .alert { +.new-stuff > .alert { border-top: 0; -webkit-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px; @@ -7034,7 +8541,7 @@ menu.pets .customize-menu .progress { } .transparent { opacity: 0.5; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; filter: alpha(opacity=50); } .col-centered { @@ -7075,7 +8582,7 @@ menu.pets .customize-menu .progress { z-index: 100; padding: 46px 0 0 0; background-color: #c5d3d6; - border-bottom: 1px solid rgba(0,0,0,0.2); + border-bottom: 1px solid rgba(0, 0, 0, 0.2); overflow-y: hidden; overflow-x: auto; } @@ -7104,7 +8611,7 @@ menu.pets .customize-menu .progress { cursor: pointer; font-weight: 400; color: #494949; - color: rgba(38,38,38,0.8); + color: rgba(38, 38, 38, 0.8); background-color: #c2d7db; } .user-menu .tile:hover, @@ -7152,7 +8659,7 @@ menu.pets .customize-menu .progress { } .stacked .tile { outline: 0; - border: 1px solid rgba(0,0,0,0.2); + border: 1px solid rgba(0, 0, 0, 0.2); border-top: 0; } .site-header { @@ -7182,7 +8689,11 @@ menu.pets .customize-menu .progress { .hero-stats .meter-label { float: left; background-color: #b0c3c7 !important; - text-shadow: -1px -1px 1px #2f3f42, 1px -1px 1px #2f3f42, -1px 1px 1px #2f3f42, 1px 1px 1px #2f3f42; + text-shadow: + -1px -1px 1px #2f3f42, + 1px -1px 1px #2f3f42, + -1px 1px 1px #2f3f42, + 1px 1px 1px #2f3f42; width: 2.618em; text-align: center; margin-right: 0.618em; @@ -7243,7 +8754,7 @@ menu.pets .customize-menu .progress { .hero-stats .meter-text.value { right: 0.382em; } -[class^="quest_"] + .hero-stats { +[class^='quest_'] + .hero-stats { min-width: 220px; padding: 1.618em 0 1em; } @@ -7706,25 +9217,33 @@ button.party-invite { border-width: 4px 0 0; } .task-column::-webkit-scrollbar-track:hover { - background-color: rgba(150,150,150,0.05); - -webkit-box-shadow: inset 1px 0 0 rgba(150,150,150,0.1); - box-shadow: inset 1px 0 0 rgba(150,150,150,0.1); + background-color: rgba(150, 150, 150, 0.05); + -webkit-box-shadow: inset 1px 0 0 rgba(150, 150, 150, 0.1); + box-shadow: inset 1px 0 0 rgba(150, 150, 150, 0.1); } .task-column::-webkit-scrollbar-track:horizontal:hover { - -webkit-box-shadow: inset 0 1px 0 rgba(150,150,150,0.1); - box-shadow: inset 0 1px 0 rgba(150,150,150,0.1); + -webkit-box-shadow: inset 0 1px 0 rgba(150, 150, 150, 0.1); + box-shadow: inset 0 1px 0 rgba(150, 150, 150, 0.1); } .task-column::-webkit-scrollbar-track:active { - background-color: rgba(150,150,150,0.05); - -webkit-box-shadow: inset 1px 0 0 rgba(150,150,150,0.14), inset -1px 0 0 rgba(150,150,150,0.07); - box-shadow: inset 1px 0 0 rgba(150,150,150,0.14), inset -1px 0 0 rgba(150,150,150,0.07); + background-color: rgba(150, 150, 150, 0.05); + -webkit-box-shadow: + inset 1px 0 0 rgba(150, 150, 150, 0.14), + inset -1px 0 0 rgba(150, 150, 150, 0.07); + box-shadow: + inset 1px 0 0 rgba(150, 150, 150, 0.14), + inset -1px 0 0 rgba(150, 150, 150, 0.07); } .task-column::-webkit-scrollbar-track:horizontal:active { - -webkit-box-shadow: inset 0 1px 0 rgba(150,150,150,0.14), inset 0 -1px 0 rgba(150,150,150,0.07); - box-shadow: inset 0 1px 0 rgba(150,150,150,0.14), inset 0 -1px 0 rgba(150,150,150,0.07); + -webkit-box-shadow: + inset 0 1px 0 rgba(150, 150, 150, 0.14), + inset 0 -1px 0 rgba(150, 150, 150, 0.07); + box-shadow: + inset 0 1px 0 rgba(150, 150, 150, 0.14), + inset 0 -1px 0 rgba(150, 150, 150, 0.07); } .task-column::-webkit-scrollbar-thumb { - background-color: rgba(150,150,150,0.2); + background-color: rgba(150, 150, 150, 0.2); -webkit-background-clip: padding; -moz-background-clip: padding; background-clip: padding-box; @@ -7732,24 +9251,32 @@ button.party-invite { border-width: 1px 1px 1px 6px; min-height: 28px; padding: 100px 0 0; - -webkit-box-shadow: inset 1px 1px 0 rgba(150,150,150,0.1), inset 0 -1px 0 rgba(150,150,150,0.07); - box-shadow: inset 1px 1px 0 rgba(150,150,150,0.1), inset 0 -1px 0 rgba(150,150,150,0.07); + -webkit-box-shadow: + inset 1px 1px 0 rgba(150, 150, 150, 0.1), + inset 0 -1px 0 rgba(150, 150, 150, 0.07); + box-shadow: + inset 1px 1px 0 rgba(150, 150, 150, 0.1), + inset 0 -1px 0 rgba(150, 150, 150, 0.07); } .task-column::-webkit-scrollbar-thumb:horizontal { border-width: 6px 1px 1px; padding: 0 0 0 100px; - -webkit-box-shadow: inset 1px 1px 0 rgba(150,150,150,0.1), inset -1px 0 0 rgba(150,150,150,0.07); - box-shadow: inset 1px 1px 0 rgba(150,150,150,0.1), inset -1px 0 0 rgba(150,150,150,0.07); + -webkit-box-shadow: + inset 1px 1px 0 rgba(150, 150, 150, 0.1), + inset -1px 0 0 rgba(150, 150, 150, 0.07); + box-shadow: + inset 1px 1px 0 rgba(150, 150, 150, 0.1), + inset -1px 0 0 rgba(150, 150, 150, 0.07); } .task-column::-webkit-scrollbar-thumb:hover { - background-color: rgba(150,150,150,0.4); - -webkit-box-shadow: inset 1px 1px 1px rgba(150,150,150,0.25); - box-shadow: inset 1px 1px 1px rgba(150,150,150,0.25); + background-color: rgba(150, 150, 150, 0.4); + -webkit-box-shadow: inset 1px 1px 1px rgba(150, 150, 150, 0.25); + box-shadow: inset 1px 1px 1px rgba(150, 150, 150, 0.25); } .task-column::-webkit-scrollbar-thumb:active { - background-color: rgba(150,150,150,0.5); - -webkit-box-shadow: inset 1px 1px 3px rgba(150,150,150,0.35); - box-shadow: inset 1px 1px 3px rgba(150,150,150,0.35); + background-color: rgba(150, 150, 150, 0.5); + -webkit-box-shadow: inset 1px 1px 3px rgba(150, 150, 150, 0.35); + box-shadow: inset 1px 1px 3px rgba(150, 150, 150, 0.35); } .task-column::-webkit-scrollbar-track { border-width: 0 1px 0 6px; @@ -7758,9 +9285,13 @@ button.party-invite { border-width: 6px 0 1px; } .task-column::-webkit-scrollbar-track:hover { - background-color: rgba(150,150,150,0.035); - -webkit-box-shadow: inset 1px 1px 0 rgba(150,150,150,0.14), inset -1px -1px 0 rgba(150,150,150,0.07); - box-shadow: inset 1px 1px 0 rgba(150,150,150,0.14), inset -1px -1px 0 rgba(150,150,150,0.07); + background-color: rgba(150, 150, 150, 0.035); + -webkit-box-shadow: + inset 1px 1px 0 rgba(150, 150, 150, 0.14), + inset -1px -1px 0 rgba(150, 150, 150, 0.07); + box-shadow: + inset 1px 1px 0 rgba(150, 150, 150, 0.14), + inset -1px -1px 0 rgba(150, 150, 150, 0.07); } .task-column::-webkit-scrollbar-thumb { border-width: 0 1px 0 6px; @@ -7817,7 +9348,7 @@ button.party-invite { } .chat-message .chat-plus-one { opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); background-color: #eee; padding: 3px 3px 0px 3px; @@ -7922,8 +9453,8 @@ button.party-invite { .party-chat markdown blockquote p:first-child { display: block; } -.tavern-chat markdown blockquote>:last-child, -.party-chat markdown blockquote>:last-child { +.tavern-chat markdown blockquote > :last-child, +.party-chat markdown blockquote > :last-child { margin-bottom: 0; } .panel-tiers div { @@ -7932,43 +9463,83 @@ button.party-invite { } .label-contributor-1 { background-color: #f57a9d !important; - text-shadow: -1px -1px 1px #660823, 1px -1px 1px #660823, -1px 1px 1px #660823, 1px 1px 1px #660823; + text-shadow: + -1px -1px 1px #660823, + 1px -1px 1px #660823, + -1px 1px 1px #660823, + 1px 1px 1px #660823; } .label-contributor-2 { background-color: #b93030 !important; - text-shadow: -1px -1px 1px #380e0e, 1px -1px 1px #380e0e, -1px 1px 1px #380e0e, 1px 1px 1px #380e0e; + text-shadow: + -1px -1px 1px #380e0e, + 1px -1px 1px #380e0e, + -1px 1px 1px #380e0e, + 1px 1px 1px #380e0e; } .label-contributor-3 { background-color: #f30 !important; - text-shadow: -1px -1px 1px #4d0f00, 1px -1px 1px #4d0f00, -1px 1px 1px #4d0f00, 1px 1px 1px #4d0f00; + text-shadow: + -1px -1px 1px #4d0f00, + 1px -1px 1px #4d0f00, + -1px 1px 1px #4d0f00, + 1px 1px 1px #4d0f00; } .label-contributor-4 { background-color: #ff9500 !important; - text-shadow: -1px -1px 1px #4d2d00, 1px -1px 1px #4d2d00, -1px 1px 1px #4d2d00, 1px 1px 1px #4d2d00; + text-shadow: + -1px -1px 1px #4d2d00, + 1px -1px 1px #4d2d00, + -1px 1px 1px #4d2d00, + 1px 1px 1px #4d2d00; } .label-contributor-5 { background-color: #fff700 !important; - text-shadow: -1px -1px 1px #4d4a00, 1px -1px 1px #4d4a00, -1px 1px 1px #4d4a00, 1px 1px 1px #4d4a00; + text-shadow: + -1px -1px 1px #4d4a00, + 1px -1px 1px #4d4a00, + -1px 1px 1px #4d4a00, + 1px 1px 1px #4d4a00; } .label-contributor-6 { background-color: #5eff00 !important; - text-shadow: -1px -1px 1px #1c4d00, 1px -1px 1px #1c4d00, -1px 1px 1px #1c4d00, 1px 1px 1px #1c4d00; + text-shadow: + -1px -1px 1px #1c4d00, + 1px -1px 1px #1c4d00, + -1px 1px 1px #1c4d00, + 1px 1px 1px #1c4d00; } .label-contributor-7 { background-color: #0af !important; - text-shadow: -1px -1px 1px #00334d, 1px -1px 1px #00334d, -1px 1px 1px #00334d, 1px 1px 1px #00334d; + text-shadow: + -1px -1px 1px #00334d, + 1px -1px 1px #00334d, + -1px 1px 1px #00334d, + 1px 1px 1px #00334d; } .label-contributor-8 { background-color: #130ead !important; - text-shadow: -1px -1px 1px #060434, 1px -1px 1px #060434, -1px 1px 1px #060434, 1px 1px 1px #060434; + text-shadow: + -1px -1px 1px #060434, + 1px -1px 1px #060434, + -1px 1px 1px #060434, + 1px 1px 1px #060434; } .label-contributor-9 { background-color: #88108f !important; - text-shadow: -1px -1px 1px #29052b, 1px -1px 1px #29052b, -1px 1px 1px #29052b, 1px 1px 1px #29052b; + text-shadow: + -1px -1px 1px #29052b, + 1px -1px 1px #29052b, + -1px 1px 1px #29052b, + 1px 1px 1px #29052b; } .label-npc { background-color: #000 !important; - text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000; + text-shadow: + -1px -1px 1px #000, + 1px -1px 1px #000, + -1px 1px 1px #000, + 1px 1px 1px #000; color: #0f0 !important; } #market-tab { @@ -8951,7 +10522,7 @@ li.spaced { } .toolbar-notifs > a span.inactive { opacity: 0.236 !important; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=24)" !important; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=24)' !important; filter: alpha(opacity=24) !important; } .toolbar-notifs div { @@ -9540,14 +11111,16 @@ noscript.banner { animation-delay: -0.2s; } @-moz-keyframes sk-bouncedelay { - 0%, 80%, 100% { + 0%, + 80%, + 100% { -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0); opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } 40% { @@ -9562,14 +11135,16 @@ noscript.banner { } } @-webkit-keyframes sk-bouncedelay { - 0%, 80%, 100% { + 0%, + 80%, + 100% { -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0); opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } 40% { @@ -9584,14 +11159,16 @@ noscript.banner { } } @-o-keyframes sk-bouncedelay { - 0%, 80%, 100% { + 0%, + 80%, + 100% { -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0); opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } 40% { @@ -9606,14 +11183,16 @@ noscript.banner { } } @keyframes sk-bouncedelay { - 0%, 80%, 100% { + 0%, + 80%, + 100% { -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0); opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; filter: alpha(opacity=0); } 40% { @@ -9646,12 +11225,12 @@ td { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - font-family: "Lato", sans-serif; + font-family: 'Lato', sans-serif; } hr { border-top: 0; border-bottom: 1px solid #ddd; - border-color: rgba(0,0,0,0.1); + border-color: rgba(0, 0, 0, 0.1); } /* Customizations to make footer sticky */ html, @@ -9688,7 +11267,7 @@ body { position: relative; } .gem-cost::before { - content: ""; + content: ''; display: block; width: 0; height: 0; @@ -9701,7 +11280,7 @@ body { margin-top: -6px; } .gem-cost::after { - content: ""; + content: ''; display: block; width: 0; height: 0; @@ -9786,7 +11365,7 @@ a.label { .buy-gems .gem-wallet .task-action-btn { -webkit-border-radius: 0 4px 0 0; border-radius: 0 4px 0 0; - border: 1px solid rgba(0,0,0,0.2); + border: 1px solid rgba(0, 0, 0, 0.2); } .badge-info { background-color: #428bca; diff --git a/www/build/static.css b/www/build/static.css index 70f4de911..51f435fe8 100644 --- a/www/build/static.css +++ b/www/build/static.css @@ -54,7 +54,7 @@ right: 0; top: 0; height: 2px; - opacity: .45; + opacity: 0.45; -moz-box-shadow: #29d 1px 0 6px 1px; -ms-box-shadow: #29d 1px 0 6px 1px; -webkit-box-shadow: #29d 1px 0 6px 1px; @@ -76,37 +76,67 @@ width: 14px; height: 14px; - border: solid 2px transparent; - border-top-color: #29d; + border: solid 2px transparent; + border-top-color: #29d; border-left-color: #29d; border-radius: 10px; -webkit-animation: loading-bar-spinner 400ms linear infinite; - -moz-animation: loading-bar-spinner 400ms linear infinite; - -ms-animation: loading-bar-spinner 400ms linear infinite; - -o-animation: loading-bar-spinner 400ms linear infinite; - animation: loading-bar-spinner 400ms linear infinite; + -moz-animation: loading-bar-spinner 400ms linear infinite; + -ms-animation: loading-bar-spinner 400ms linear infinite; + -o-animation: loading-bar-spinner 400ms linear infinite; + animation: loading-bar-spinner 400ms linear infinite; } @-webkit-keyframes loading-bar-spinner { - 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } @-moz-keyframes loading-bar-spinner { - 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } } @-o-keyframes loading-bar-spinner { - 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } } @-ms-keyframes loading-bar-spinner { - 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } + 0% { + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } } @keyframes loading-bar-spinner { - 0% { transform: rotate(0deg); transform: rotate(0deg); } - 100% { transform: rotate(360deg); transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + transform: rotate(360deg); + } } .subscription-features tr td { @@ -256,7 +286,7 @@ body { .muted i, i.muted { opacity: 0.5; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; filter: alpha(opacity=50); } #header-play-button { @@ -302,13 +332,13 @@ a.h2.accordion { a.h2.accordion:before { font-family: 'Glyphicons Halflings'; color: #808080; - content: "\e114"; + content: '\e114'; margin-right: 0.5em; } a.h2.accordion.collapsed:before { font-family: 'Glyphicons Halflings'; color: #808080; - content: "\e080"; + content: '\e080'; margin-right: 0.5em; } .merch-block { diff --git a/www/css/main.diary.css b/www/css/main.diary.css index 474d57256..4fad2e975 100644 --- a/www/css/main.diary.css +++ b/www/css/main.diary.css @@ -1,78 +1,72 @@ .no-margin { - margin:0px !important; - padding:0px !important; + margin: 0px !important; + padding: 0px !important; } .item { - border:0px !important; + border: 0px !important; border-color: #fff; padding: 0 10px; /* Changed from 16px. This change was to ensure the correct alignment of the diary card */ } .main { - padding-top:50px; + padding-top: 50px; min-height: 100%; } .row { - padding:0px; + padding: 0px; } .col { - padding:0px !important; + padding: 0px !important; } .small { - font-size:7px; + font-size: 7px; } -.bg-color{ - background:#71bc98!important; - color:whitesmoke !important; +.bg-color { + background: #71bc98 !important; + color: whitesmoke !important; } -.summary-color{ - background:#1b9e77!important; - color:whitesmoke !important; +.summary-color { + background: #1b9e77 !important; + color: whitesmoke !important; } -.place-color{ - background:#7570b3!important; - color:whitesmoke !important; +.place-color { + background: #7570b3 !important; + color: whitesmoke !important; } - /* leaflet */ /* ----------- iPhone 5 and 5S ----------- */ /* Portrait and Landscape */ -@media only screen - and (min-device-width: 320px) - and (max-device-width: 568px) - and (-webkit-min-device-pixel-ratio: 2) { - .angular-leaflet-map { width: 100%; } +@media only screen and (min-device-width: 320px) and (max-device-width: 568px) and (-webkit-min-device-pixel-ratio: 2) { + .angular-leaflet-map { + width: 100%; + } } /* ----------- iPhone 6 ----------- */ /* Portrait and Landscape */ -@media only screen - and (min-device-width: 375px) - and (max-device-width: 667px) - and (-webkit-min-device-pixel-ratio: 2) { - .angular-leaflet-map { width: 100%; } +@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (-webkit-min-device-pixel-ratio: 2) { + .angular-leaflet-map { + width: 100%; + } } /* ----------- iPhone 6+ ----------- */ /* Portrait and Landscape */ -@media only screen - and (min-device-width: 414px) - and (max-device-width: 736px) - and (-webkit-min-device-pixel-ratio: 3) { - .angular-leaflet-map { width: 100%; } +@media only screen and (min-device-width: 414px) and (max-device-width: 736px) and (-webkit-min-device-pixel-ratio: 3) { + .angular-leaflet-map { + width: 100%; + } } - .list .item.item-accordion { - transition: 0.09s all linear; } .list .item.item-accordion.ng-hide { @@ -110,16 +104,18 @@ a.item-content { font-size: 13px; line-height: 1; font-weight: 500; - box-shadow: 0 1px 2px rgb(0 0 0 / .1), 0 2px 3px rgb(0 0 0 / .12); + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.1), + 0 2px 3px rgb(0 0 0 / 0.12); background-color: white; color: var(--accent-dark); - border: .12em solid var(--accent); + border: 0.12em solid var(--accent); } .diary-btn-yellow, .diary-btn-yellow:hover, .diary-btn-yellow:active { - background-color: #FFC108; /* tentatively orange for now */ + background-color: #ffc108; /* tentatively orange for now */ color: white; border: 2px solid rgba(0, 136, 206, 0.2); } @@ -135,7 +131,7 @@ a.item-content { .diary-btn:before { font-weight: bold; scale: 1.7; - margin-right: .6em; + margin-right: 0.6em; line-height: 100%; } @@ -188,19 +184,20 @@ a.item-content { font-size: 13px; line-height: 1.2; margin: 0; - border: 1px solid rgb(0 0 0 / .2); + border: 1px solid rgb(0 0 0 / 0.2); box-sizing: border-box; border-radius: 30px; position: relative; - box-shadow: 0 3px 4px rgb(0 0 0 / 5%), 0 4px 4px rgb(0 0 0 / 8%); + box-shadow: + 0 3px 4px rgb(0 0 0 / 5%), + 0 4px 4px rgb(0 0 0 / 8%); display: flex; flex-wrap: wrap; - background: linear-gradient(40deg, - hsla(200, 30%, 97%, 1) 40%, - hsla(0, 0%, 100%, 1)), + background: linear-gradient(40deg, hsla(200, 30%, 97%, 1) 40%, hsla(0, 0%, 100%, 1)); } -.diary-card.place, .diary-card.untracked { +.diary-card.place, +.diary-card.untracked { color: #222; background: hsl(200 100% 85%); border: 1px solid hsl(200 100% 10% / 0.2); @@ -209,23 +206,23 @@ a.item-content { } .diary-card.untracked { - color: #333; - /* untracked time will have a reddish color */ - --accent: hsl(350, 25%, 50%); - --accent-light: hsl(350, 65%, 85%); - --accent-dark: hsl(350, 65%, 30%); - - --grid: hsla(350, 25%, 80%, .2); - /* subtle x-grid lines in the background, fading to white */ - background: linear-gradient(15deg, - hsla(350, 10%, 92%, 1) 40%, - hsla(350, 10%, 100%, 0.5)), - repeating-linear-gradient(45deg, - var(--grid), var(--grid) 0px, - transparent 2px, transparent 20px), - repeating-linear-gradient(-45deg, - var(--grid), var(--grid) 0px, - #fff 2px, #fff 21px); + color: #333; + /* untracked time will have a reddish color */ + --accent: hsl(350, 25%, 50%); + --accent-light: hsl(350, 65%, 85%); + --accent-dark: hsl(350, 65%, 30%); + + --grid: hsla(350, 25%, 80%, 0.2); + /* subtle x-grid lines in the background, fading to white */ + background: linear-gradient(15deg, hsla(350, 10%, 92%, 1) 40%, hsla(350, 10%, 100%, 0.5)), + repeating-linear-gradient( + 45deg, + var(--grid), + var(--grid) 0px, + transparent 2px, + transparent 20px + ), + repeating-linear-gradient(-45deg, var(--grid), var(--grid) 0px, #fff 2px, #fff 21px); } .diary-card.untracked .card-title b { @@ -236,23 +233,24 @@ a.item-content { border-radius: 5px; } -.diary-card.draft, .diary-details.draft { +.diary-card.draft, +.diary-details.draft { /* draft trips will have a muted, greenish color */ --accent: hsl(150, 15%, 40%); --accent-light: hsl(150, 25%, 72%); --accent-dark: hsl(150, 35%, 30%); - --grid: hsla(150, 25%, 70%, .3); + --grid: hsla(150, 25%, 70%, 0.3); /* subtle grid lines in the background, fading to white */ - background: linear-gradient(30deg, - hsla(150, 4%, 94%, .9) 50%, - hsla(0, 0%, 100%, .5)), - repeating-linear-gradient(90deg, - var(--grid), var(--grid) 0px, - transparent 2px, transparent 20px), - repeating-linear-gradient(0deg, - var(--grid), var(--grid) 0px, - #fff 2px, #fff 21px); + background: linear-gradient(30deg, hsla(150, 4%, 94%, 0.9) 50%, hsla(0, 0%, 100%, 0.5)), + repeating-linear-gradient( + 90deg, + var(--grid), + var(--grid) 0px, + transparent 2px, + transparent 20px + ), + repeating-linear-gradient(0deg, var(--grid), var(--grid) 0px, #fff 2px, #fff 21px); } .card-title { @@ -304,7 +302,7 @@ a.item-content { @media screen and (orientation: portrait) { .hr-lines:before { - content: " "; + content: ' '; display: block; height: 2px; width: 30%; @@ -315,7 +313,7 @@ a.item-content { } .hr-lines:after { - content: " "; + content: ' '; display: block; height: 2px; width: 30%; @@ -327,7 +325,7 @@ a.item-content { } @media screen and (orientation: landscape) { .hr-lines:before { - content: " "; + content: ' '; display: block; height: 2px; width: 41%; @@ -338,7 +336,7 @@ a.item-content { } .hr-lines:after { - content: " "; + content: ' '; display: block; height: 2px; width: 41%; @@ -387,12 +385,13 @@ a.item-content { z-index: 0; } -.diary-map, .diary-map * { +.diary-map, +.diary-map * { pointer-events: none !important; } /* when trip notes are enabled, the map has rounded right corners */ -.enhanced-trip-item .diary-map > div{ +.enhanced-trip-item .diary-map > div { border-radius: 30px 0px 30px 0px; } @@ -479,11 +478,13 @@ a.item-content { background-color: var(--accent) !important; } -.ionic_datepicker_popup .popup-body .month_select, .ionic_datepicker_popup .popup-body .year_select { +.ionic_datepicker_popup .popup-body .month_select, +.ionic_datepicker_popup .popup-body .year_select { border-bottom: 1px solid var(--accent) !important; } -.ionic_datepicker_popup .popup-body .month_select:after, .ionic_datepicker_popup .popup-body .year_select:after { +.ionic_datepicker_popup .popup-body .month_select:after, +.ionic_datepicker_popup .popup-body .year_select:after { color: var(--accent) !important; } @@ -502,13 +503,15 @@ div.labelfilterlist { color: var(--accent); border-radius: 0px; border-width: 0; - box-shadow: 0 1px 2px rgba(0,0,0,0.16), 0 2px 2px rgba(0,0,0,0.23); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.16), + 0 2px 2px rgba(0, 0, 0, 0.23); padding: 0 0.1em !important; } .button.labelfilter.on { - background-color: var(--accent); - color: white; + background-color: var(--accent); + color: white; } .labelfilter:first-of-type { diff --git a/www/css/style.css b/www/css/style.css index dea003e7b..a2ac29368 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -6,8 +6,8 @@ .question.non-select { display: inline-block; } - .question input[name*="_date"], - .question input[name*="_time"] { + .question input[name*='_date'], + .question input[name*='_time'] { width: calc(40vw - 10px); margin-right: 5px; display: flex; @@ -15,7 +15,7 @@ } .enketo-plugin .form-header { - max-height: 50px; + max-height: 50px; } .fill-container { @@ -67,19 +67,21 @@ label-tab > div { text-align: center; } -[ng\:cloak], [ng-cloak], .ng-cloak { +[ng\:cloak], +[ng-cloak], +.ng-cloak { display: none !important; } .popup-title { - color: #6e6e6e; + color: #6e6e6e; } .pull-right { - float: right + float: right; } .pull-left { - float: left + float: left; } .button.button-icon.ion-help:before { @@ -87,14 +89,13 @@ label-tab > div { } .popup-buttons.row { - height: 40px !important; + height: 40px !important; } .popup-buttons.button { height: 40px !important; } .button.ng-binding.button-stable { height: 40px; - } .button.ng-binding.button-positive { background-color: var(--accent); @@ -103,31 +104,29 @@ label-tab > div { .button.ng-binding.button-assertive { background-color: var(--accent); height: 40px; - } .button.ng-binding.button-cancel { background-color: #d02001; height: 40px; - color: #ffffff + color: #ffffff; } .selected_date_full.ng-binding { color: var(--accent); } .icon.ion-chevron-left { - color: var(--accent); + color: var(--accent); } .icon.ion-chevron-right { - color: var(--accent); + color: var(--accent); } .date_col.date_selected { - background-color: var(--accent) !important; - + background-color: var(--accent) !important; } .date_col:active { - background-color: var(--accent) !important; + background-color: var(--accent) !important; } .customButtomIconSize:before { - font-size: 25px !important; + font-size: 25px !important; } #dashboard-footprint.card { @@ -138,7 +137,9 @@ label-tab > div { margin: 10px; margin-top: 55px; position: relative; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03), 0 2px 3px rgba(0, 0, 0, 0.05); + box-shadow: + 0 2px 3px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); /*background: rgba(40,218,183,1); background: -moz-linear-gradient(-45deg, rgba(40,218,183,1) 0%, rgba(99,230,199,1) 30%, rgba(94,235,212,1) 51%, rgba(87,235,223,1) 69%, rgba(99,237,226,1) 85%, rgba(127,250,250,1) 100%); background: -webkit-gradient(left top, right bottom, color-stop(0%, rgba(40,218,183,1)), color-stop(30%, rgba(99,230,199,1)), color-stop(51%, rgba(94,235,212,1)), color-stop(69%, rgba(87,235,223,1)), color-stop(85%, rgba(99,237,226,1)), color-stop(100%, rgba(127,250,250,1))); @@ -152,11 +153,11 @@ label-tab > div { overflow: hidden; } -.small-footprint-card{ +.small-footprint-card { height: 140px !important; } -.expanded-footprint-card{ +.expanded-footprint-card { height: 460px !important; } @@ -167,7 +168,9 @@ label-tab > div { display: block; margin: 10px; position: relative; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03), 0 2px 3px rgba(0, 0, 0, 0.05); + box-shadow: + 0 2px 3px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); /*background: rgba(40,218,183,1); background: -moz-linear-gradient(-45deg, rgba(40,218,183,1) 0%, rgba(99,230,199,1) 30%, rgba(94,235,212,1) 51%, rgba(87,235,223,1) 69%, rgba(99,237,226,1) 85%, rgba(127,250,250,1) 100%); background: -webkit-gradient(left top, right bottom, color-stop(0%, rgba(40,218,183,1)), color-stop(30%, rgba(99,230,199,1)), color-stop(51%, rgba(94,235,212,1)), color-stop(69%, rgba(87,235,223,1)), color-stop(85%, rgba(99,237,226,1)), color-stop(100%, rgba(127,250,250,1))); @@ -181,11 +184,11 @@ label-tab > div { overflow: hidden; } -.small-calorie-card{ +.small-calorie-card { height: 140px !important; } -.expanded-calorie-card{ +.expanded-calorie-card { height: 370px !important; } @@ -210,10 +213,12 @@ label-tab > div { display: block; /* height: 140px; */ margin: 10px; - margin-top:0px; + margin-top: 0px; position: relative; margin-bottom: 5px !important; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03), 0 2px 3px rgba(0, 0, 0, 0.05); + box-shadow: + 0 2px 3px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); /*background: rgba(40,218,183,1); background: -moz-linear-gradient(-45deg, rgba(40,218,183,1) 0%, rgba(99,230,199,1) 30%, rgba(94,235,212,1) 51%, rgba(87,235,223,1) 69%, rgba(99,237,226,1) 85%, rgba(127,250,250,1) 100%); background: -webkit-gradient(left top, right bottom, color-stop(0%, rgba(40,218,183,1)), color-stop(30%, rgba(99,230,199,1)), color-stop(51%, rgba(94,235,212,1)), color-stop(69%, rgba(87,235,223,1)), color-stop(85%, rgba(99,237,226,1)), color-stop(100%, rgba(127,250,250,1))); @@ -225,12 +230,12 @@ label-tab > div { text-align: center; } -#arrow-color{ +#arrow-color { color: var(--accent); font-size: 25px !important; } -h4.dashboard-headers{ +h4.dashboard-headers { color: #fff; background: var(--accent); padding-top: 5px; @@ -241,55 +246,55 @@ h4.dashboard-headers{ margin-bottom: 0px !important; } -.user-carbon-no-percentage{ +.user-carbon-no-percentage { padding-top: 30px; position: absolute; width: 100%; } -.user-carbon-percentage{ +.user-carbon-percentage { padding-top: 10px; position: absolute; width: 100%; } -.user-carbon{ +.user-carbon { font-weight: 700; color: var(--accent); font-size: 16px; } -.user-calorie-no-percentage{ +.user-calorie-no-percentage { padding-top: 30px; position: absolute; width: 100%; } -.user-calorie-percentage{ +.user-calorie-percentage { padding-top: 10px; position: absolute; width: 100%; } -.user-calorie{ +.user-calorie { font-weight: 700; color: var(--accent); font-size: 18px; } -.percentage-change{ +.percentage-change { font-weight: 700; color: var(--accent); margin-bottom: 20px; } -.calorie-change{ +.calorie-change { padding-top: 5px; font-weight: 700; color: var(--accent); } -.dashboard-list{ +.dashboard-list { padding-top: 10px; font-weight: 700; color: #fff; @@ -306,7 +311,9 @@ h4.dashboard-headers{ width: 60px; height: 60px; background: #fff; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03), 0 2px 3px rgba(0, 0, 0, 0.05); + box-shadow: + 0 2px 3px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); -moz-border-radius: 50px; -webkit-border-radius: 50px; border-radius: 50px; @@ -316,43 +323,47 @@ h4.dashboard-headers{ margin-top: 10px; } -#circle-food.circle{ +#circle-food.circle { position: relative !important; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1), 0 2px 3px rgba(0, 0, 0, 0.05) !important; - margin:auto; + box-shadow: + 0 2px 6px rgba(0, 0, 0, 0.1), + 0 2px 3px rgba(0, 0, 0, 0.05) !important; + margin: auto; float: none; display: inline-block; margin-right: 20px; margin-top: 20px; } -#circle-food.circle:active{ +#circle-food.circle:active { background-color: #eeeeee; - box-shadow: 0 0px 0px rgba(0, 0, 0, 0.1), 0 0px 0px rgba(0, 0, 0, 0.05) !important; + box-shadow: + 0 0px 0px rgba(0, 0, 0, 0.1), + 0 0px 0px rgba(0, 0, 0, 0.05) !important; } -#green-leaf{ +#green-leaf { color: var(--accent-light); font-size: 45px; padding-top: 5px; } -#food{ +#food { width: 45px; padding-top: 7px; } -#foodB{ +#foodB { width: 45px; padding-top: 7px; padding-right: 4px; } -.arrow-position{ +.arrow-position { position: absolute; bottom: 5px; right: 10px; - color:#b2b2b2; + color: #b2b2b2; font-size: 20px; } @@ -360,9 +371,9 @@ h4.dashboard-headers{ height:245px !important; } */ -#modes.slider-slide{ +#modes.slider-slide { padding-top: 0 !important; - background-color:transparent; + background-color: transparent; } /*.ion-view-background-dashboard{ @@ -376,12 +387,14 @@ h4.dashboard-headers{ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#87f5f5', endColorstr='#5ffad8', GradientType=0 ); }*/ -.bar.bar-header.no-bgColor, .bar.bar-footer.no-bgColor{ -border: 0px !important; -border-color: transparent !important; -border-top: transparent !important; -border-bottom: transparent !important; -background-image: none !important; } +.bar.bar-header.no-bgColor, +.bar.bar-footer.no-bgColor { + border: 0px !important; + border-color: transparent !important; + border-top: transparent !important; + border-bottom: transparent !important; + background-image: none !important; +} .list .item.item-accordion { line-height: 38px; @@ -414,24 +427,24 @@ background-image: none !important; } background: white; border-radius: 50px; color: #222; - border: 1px solid rgb(0 0 0 / .2); + border: 1px solid rgb(0 0 0 / 0.2); padding: 3px 20px; margin: auto; display: block; } /* Light theme */ -.control-icon-button{ +.control-icon-button { text-align: center; max-height: 56px; background-color: #6c757d; color: #fff; padding-top: 16px; width: 64px; - font-size:20px; + font-size: 20px; } -.diary-button{ +.diary-button { text-align: center; float: right; height: 48px; @@ -442,7 +455,7 @@ background-image: none !important; } width: 48px; /* Changed to fit the diary card in full view */ } -.control-version-number{ +.control-version-number { text-align: center; float: right; height: 100%; @@ -450,32 +463,37 @@ background-image: none !important; } padding-top: 16px; width: 64px; - font-size:20px; + font-size: 20px; } -#switch-user.control-icon-button{ +#switch-user.control-icon-button { background-color: #dc3545 !important; } -.gray-icon.control-icon-button{ - background-color: #CCCCCC !important; +.gray-icon.control-icon-button { + background-color: #cccccc !important; } -.toggle-on-ourcolor-bg{ +.toggle-on-ourcolor-bg { border-color: var(--accent) !important; background-color: var(--accent) !important; } -.control-info{ +.control-info { padding: 2px 4px !important; } -.tab-nav{ - background-color: #f5f5f5 !important; background-size: 0 !important; - box-shadow: 0 1px 2px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); -} -.tab-item.tab-item-active, .tab-item.active, .tab-item.activated { - color: var(--accent); +.tab-nav { + background-color: #f5f5f5 !important; + background-size: 0 !important; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.16), + 0 3px 6px rgba(0, 0, 0, 0.23); +} +.tab-item.tab-item-active, +.tab-item.active, +.tab-item.activated { + color: var(--accent); } .platform-ios.platform-cordova:not(.fullscreen) .bar-header:not(.bar-subheader) > * { margin-top: 15px; @@ -485,7 +503,9 @@ background-image: none !important; } } ion-header-bar { background-color: #f5f5f5 !important; - box-shadow: 0 1px 2px rgb(0 0 0 / 8%), 0 3px 6px rgb(0 0 0 / 12%); + box-shadow: + 0 1px 2px rgb(0 0 0 / 8%), + 0 3px 6px rgb(0 0 0 / 12%); } ion-nav-view { z-index: 10; @@ -496,29 +516,37 @@ ion-nav-view { } .tabs-custom > .tabs, .tabs.tabs-custom { - border-color: #5D3A23; - background-color: #5D3A23; + border-color: #5d3a23; + background-color: #5d3a23; background-image: linear-gradient(0deg, #0c60ee, #0c60ee 70%, transparent 70%); - color: #999; } - .tabs-custom > .tabs .tab-item .badge, - .tabs.tabs-custom .tab-item .badge { - background-color: #999; - color: #387ef5; } + color: #999; +} +.tabs-custom > .tabs .tab-item .badge, +.tabs.tabs-custom .tab-item .badge { + background-color: #999; + color: #387ef5; +} .tabs-striped.tabs-custom .tabs { - background-color: #5D3A23; } + background-color: #5d3a23; +} .tabs-striped.tabs-custom .tab-item { color: rgba(255, 255, 255, 0.7); - opacity: 1; } - .tabs-striped.tabs-custom .tab-item .badge { - opacity: 0.7; } - .tabs-striped.tabs-custom .tab-item.tab-item-active, .tabs-striped.tabs-custom .tab-item.active, .tabs-striped.tabs-positive .tab-item.activated { - margin-top: -2px; - color: #fff; - border-style: solid; - border-width: 2px 0 0 0; - border-color: #fff; } + opacity: 1; +} +.tabs-striped.tabs-custom .tab-item .badge { + opacity: 0.7; +} +.tabs-striped.tabs-custom .tab-item.tab-item-active, +.tabs-striped.tabs-custom .tab-item.active, +.tabs-striped.tabs-positive .tab-item.activated { + margin-top: -2px; + color: #fff; + border-style: solid; + border-width: 2px 0 0 0; + border-color: #fff; +} .title.title-center.header-item { color: #303030; } @@ -526,9 +554,15 @@ ion-nav-view { display: none; } .date-picker-button { - color: var(--accent) !important; padding: 0 15px; border-color: transparent; margin-top: 4px; - /* box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); */ - border-style: solid; border-color: white; border-width: 0px; border-radius: 5px; + color: var(--accent) !important; + padding: 0 15px; + border-color: transparent; + margin-top: 4px; + /* box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); */ + border-style: solid; + border-color: white; + border-width: 0px; + border-radius: 5px; } .button.date-picker-button { @@ -536,11 +570,12 @@ ion-nav-view { } .date-picker-arrow { - color: #303030 !important; margin-top: 4px; background-color: transparent !important; + color: #303030 !important; + margin-top: 4px; + background-color: transparent !important; } /* Light theme ends */ - .earlier-later-expand { color: #303030; margin: 16px 16px 0 6px; @@ -555,7 +590,7 @@ ion-nav-view { padding-bottom: 5px; padding-left: 30px; margin-top: 0 !important; - margin-bottom: 0!important; + margin-bottom: 0 !important; } p.list-text { color: #303030; @@ -570,38 +605,52 @@ a.list-text { } .card-1 { - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); - transition: all 0.3s cubic-bezier(.25,.8,.25,1); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } .card-1:hover { - box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); + box-shadow: + 0 14px 28px rgba(0, 0, 0, 0.25), + 0 10px 10px rgba(0, 0, 0, 0.22); } .card-2 { - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + box-shadow: + 0 3px 6px rgba(0, 0, 0, 0.16), + 0 3px 6px rgba(0, 0, 0, 0.23); } .card-3 { - box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); + box-shadow: + 0 10px 20px rgba(0, 0, 0, 0.19), + 0 6px 6px rgba(0, 0, 0, 0.23); } .card-4 { - box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); + box-shadow: + 0 14px 28px rgba(0, 0, 0, 0.25), + 0 10px 10px rgba(0, 0, 0, 0.22); } .card-5 { - box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); + box-shadow: + 0 19px 38px rgba(0, 0, 0, 0.3), + 0 15px 12px rgba(0, 0, 0, 0.22); } button.button.ng-binding i.icon.ion-edit { font-size: 12px; } button.button.back-button.buttons.button-clear.header-item { - color: #303030; opacity: 0.7; + color: #303030; + opacity: 0.7; } .nav-bar-title { - color: #303030; opacity: 0.7; + color: #303030; + opacity: 0.7; } /* Profile tab */ .control-list-item { @@ -619,20 +668,25 @@ button.button.back-button.buttons.button-clear.header-item { display: -webkit-box; line-height: 1.1; -webkit-line-clamp: 5; /* number of lines to show */ - line-clamp: 5; + line-clamp: 5; -webkit-box-orient: vertical; text-overflow: ellipsis; } .control-list-toggle { - float: right; margin-top: 5px; margin-right: 2px; + float: right; + margin-top: 5px; + margin-right: 2px; } /* Diary list tab */ .lightrail { - color: blue + color: blue; } .dev-zone-input { - padding: 7px 0; font-size: 16px; line-height: 22px; height: 36px; + padding: 7px 0; + font-size: 16px; + line-height: 22px; + height: 36px; } .dev-zone-title { padding: 18px 16px; @@ -644,14 +698,16 @@ button.button.back-button.buttons.button-clear.header-item { } .list-card { margin: 16px 0; - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + box-shadow: + 0 3px 6px rgba(0, 0, 0, 0.16), + 0 3px 6px rgba(0, 0, 0, 0.23); border: 1px solid #ccc; } .bg-light { - background-color: #ffffff; + background-color: #ffffff; } .bg-unprocessed { - background-color: #9eb2aa; + background-color: #9eb2aa; } .list-card-sm { width: 95%; @@ -660,20 +716,38 @@ button.button.back-button.buttons.button-clear.header-item { width: 95%; } .list-card-lg { - width: 95%; + width: 95%; } .list-card .row { padding-left: 5px; padding-right: 5px; } .list-col-left-margin { - text-align: center; padding: 0.7em 0.8em 0.4em 0.8em; border-right-width: 0.5px; border-right-color: #ccc; border-right-style: solid; border-bottom-color: #ccc; border-bottom-width: 0.5px; border-bottom-style: solid; + text-align: center; + padding: 0.7em 0.8em 0.4em 0.8em; + border-right-width: 0.5px; + border-right-color: #ccc; + border-right-style: solid; + border-bottom-color: #ccc; + border-bottom-width: 0.5px; + border-bottom-style: solid; } .list-col-left { - text-align: center; padding: 1.1em 0.8em 0.6em 0.8em; border-right-width: 0.5px; border-right-color: #ccc; border-right-style: solid; border-bottom-color: #ccc; border-bottom-width: 0.5px; border-bottom-style: solid; + text-align: center; + padding: 1.1em 0.8em 0.6em 0.8em; + border-right-width: 0.5px; + border-right-color: #ccc; + border-right-style: solid; + border-bottom-color: #ccc; + border-bottom-width: 0.5px; + border-bottom-style: solid; } .list-col-right { - text-align: center; padding: 0.25em 0.8em; border-bottom-color: #ccc; border-bottom-width: 0.5px; border-bottom-style: solid; + text-align: center; + padding: 0.25em 0.8em; + border-bottom-color: #ccc; + border-bottom-width: 0.5px; + border-bottom-style: solid; } timestamp-badge { @@ -703,15 +777,15 @@ timestamp-badge[light-bg] { } .diary-checkmark-container i.can-verify { - color: #30A64A; + color: #30a64a; background-color: #ddd; border-radius: 5px; } .diary-checkmark-container i.cannot-verify { - color: #E6B8B8; + color: #e6b8b8; } .diary-checkmark-container i.already-verified { - color: #B8E6C2; + color: #b8e6c2; } /* .diary-checkmark-container i.already-verified, .diary-checkmark-container i.cannot-verify { color: #BFBFBF; @@ -740,22 +814,24 @@ timestamp-badge[light-bg] { .metric-datepicker { /*height: 33px;*/ - display: flex; /* establish flex container */ + display: flex; /* establish flex container */ /*flex-direction: column; make main axis vertical */ justify-content: center; /* center items vertically, in this case */ - align-items: center; /* center items horizontally, in this case */ + align-items: center; /* center items horizontally, in this case */ border-radius: 5px; background-color: white; - box-shadow: 0 1px 1px rgba(0,0,0,0.03), 0 1px 1px rgba(0,0,0,0.05); + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.03), + 0 1px 1px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; } .metric-title { height: 35px; - display: flex; /* establish flex container */ - flex-direction: column; /* make main axis vertical */ + display: flex; /* establish flex container */ + flex-direction: column; /* make main axis vertical */ justify-content: center; /* center items vertically, in this case */ - align-items: left; /* center items horizontally, in this case */ + align-items: left; /* center items horizontally, in this case */ padding-left: 10px; } @@ -768,7 +844,9 @@ timestamp-badge[light-bg] { float: right; border-radius: 5px; background-color: white; - box-shadow: 0 1px 1px rgba(0,0,0,0.03), 0 1px 1px rgba(0,0,0,0.05); + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.03), + 0 1px 1px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; } @@ -778,7 +856,9 @@ timestamp-badge[light-bg] { top: 40px; border-radius: 5px; background-color: white; - box-shadow: 0 1px 1px rgba(0,0,0,0.03), 0 1px 1px rgba(0,0,0,0.05); + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.03), + 0 1px 1px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; } @@ -796,17 +876,19 @@ timestamp-badge[light-bg] { width: 75%; float: right; } -.metric-change-data-button{ +.metric-change-data-button { margin: auto; width: 120px; border-radius: 20px; background-color: white; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03), 0 2px 3px rgba(0, 0, 0, 0.05); + box-shadow: + 0 2px 3px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); color: var(--accent); font-weight: 700; height: 30px; } -.metric-change-data-button:active{ +.metric-change-data-button:active { background-color: var(--accent); color: white; box-shadow: none; @@ -816,7 +898,9 @@ timestamp-badge[light-bg] { width: 49%; border-radius: 5px; background-color: white; - box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.05); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.03), + 0 1px 2px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; } @@ -825,22 +909,23 @@ timestamp-badge[light-bg] { width: 33%; border-radius: 5px; background-color: white; - box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.05); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.03), + 0 1px 2px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; - } .current-mode-button { border: none; - background-color:#2D9CDB; - display:inline-block; - cursor:pointer; - color:#ffffff; + background-color: #2d9cdb; + display: inline-block; + cursor: pointer; + color: #ffffff; opacity: 0.4; - font-size:28px; + font-size: 28px; width: 100%; - text-decoration:none; + text-decoration: none; height: 80px; z-index: 1; position: relative; @@ -854,11 +939,13 @@ timestamp-badge[light-bg] { display: block; width: 40%; height: 25px; - background-color: #f5f5f5;; + background-color: #f5f5f5; border-radius: 10px; - color: #6A6A6A; + color: #6a6a6a; left: 30%; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.05), + 0 1px 2px rgba(0, 0, 0, 0.05); } #current-start-time-text { @@ -876,8 +963,10 @@ timestamp-badge[light-bg] { } #current-speed { - background-color: #8F8F8F; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.05); + background-color: #8f8f8f; + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.2), + 0 2px 4px rgba(0, 0, 0, 0.05); opacity: 0.9; color: white; width: 60px; @@ -885,7 +974,7 @@ timestamp-badge[light-bg] { height: 60px; border-style: solid; border-radius: 50%; - border-color: #6A6A6A; + border-color: #6a6a6a; border-width: 4px; } @@ -901,7 +990,7 @@ timestamp-badge[light-bg] { } #current-direction-text { - color:#6A6A6A; + color: #6a6a6a; font-size: 20px; font-weight: 600; margin-top: 5px; @@ -917,21 +1006,25 @@ timestamp-badge[light-bg] { .report-button { border-radius: 10px; border: none; - background-color: #E34949; + background-color: #e34949; color: #ffffff; font-size: 20px; width: 60%; height: 35px; z-index: 1; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.05); + box-shadow: + 0 4px 8px rgba(0, 0, 0, 0.3), + 0 2px 4px rgba(0, 0, 0, 0.05); position: absolute; display: block; bottom: 40px; left: 20%; } -.report-button:active{ - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(0, 0, 0, 0.05) !important; +.report-button:active { + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.1), + 0 1px 1px rgba(0, 0, 0, 0.05) !important; } .metric-freq-button { @@ -939,7 +1032,9 @@ timestamp-badge[light-bg] { width: 100%; border-radius: 5px; background-color: white; - box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.05); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.03), + 0 1px 2px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; } @@ -959,25 +1054,30 @@ timestamp-badge[light-bg] { height: 35px; } .hvcenter { - display: flex; /* establish flex container */ - flex-direction: column; /* make main axis vertical */ + display: flex; /* establish flex container */ + flex-direction: column; /* make main axis vertical */ justify-content: center; /* center items vertically, in this case */ - align-items: center; /* center items horizontally, in this case */ + align-items: center; /* center items horizontally, in this case */ } .metric-basic { width: 100%; border-radius: 5px; background-color: white; - box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.05); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.03), + 0 1px 2px rgba(0, 0, 0, 0.05); color: var(--accent); height: 35px; } .metric-half { - float: left; + float: left; width: 100%; border-radius: 5px; background-color: white; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.03), 0 2px 3px rgba(0, 0, 0, 0.05); color: #01D0A7; + box-shadow: + 0 2px 3px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); + color: #01d0a7; height: 30px; overflow: hidden; position: relative; @@ -1095,8 +1195,8 @@ timestamp-badge[light-bg] { border-top-left-radius: 5px; border-bottom-left-radius: 5px; } -.distance-button{ - width:25%; +.distance-button { + width: 25%; font-size: 12px; font-weight: 700; overflow: hidden; @@ -1105,8 +1205,8 @@ timestamp-badge[light-bg] { float: left; height: 30px; } -.speed-button{ - width:25%; +.speed-button { + width: 25%; font-size: 12px; font-weight: 700; overflow: hidden; @@ -1115,24 +1215,24 @@ timestamp-badge[light-bg] { float: left; height: 30px; } -.trips-button{ - width:25%; +.trips-button { + width: 25%; font-size: 12px; font-weight: 700; overflow: hidden; float: left; height: 30px; } -.duration-button{ - width:25%; +.duration-button { + width: 25%; font-size: 12px; font-weight: 700; overflow: hidden; float: left; height: 30px; } -.distance-button-active{ - width:25%; +.distance-button-active { + width: 25%; font-size: 12px; font-weight: 700; background-color: var(--accent); @@ -1143,8 +1243,8 @@ timestamp-badge[light-bg] { float: left; height: 30px; } -.speed-button-active{ - width:25%; +.speed-button-active { + width: 25%; font-size: 12px; font-weight: 700; overflow: hidden; @@ -1155,8 +1255,8 @@ timestamp-badge[light-bg] { float: left; height: 30px; } -.trips-button-active{ - width:25%; +.trips-button-active { + width: 25%; font-size: 12px; font-weight: 700; background-color: var(--accent); @@ -1165,8 +1265,8 @@ timestamp-badge[light-bg] { float: left; height: 30px; } -.duration-button-active{ - width:25%; +.duration-button-active { + width: 25%; font-size: 12px; font-weight: 700; background-color: var(--accent); @@ -1182,7 +1282,6 @@ timestamp-badge[light-bg] { height: 33px; } .metric-me-toggle { - } .metric-icon { color: #ccc; @@ -1192,10 +1291,10 @@ timestamp-badge[light-bg] { position: absolute; width: 15%; height: 35px; - display: flex; /* establish flex container */ - flex-direction: column; /* make main axis vertical */ + display: flex; /* establish flex container */ + flex-direction: column; /* make main axis vertical */ justify-content: center; /* center items vertically, in this case */ - align-items: left; /* center items horizontally, in this case */ + align-items: left; /* center items horizontally, in this case */ padding-left: 10px; } .metric-filter-year { @@ -1204,7 +1303,9 @@ timestamp-badge[light-bg] { left: 21%; height: 35px; border-width: 0; - box-shadow: 0 1px 1px rgba(0,0,0,0.03), 0 1px 1px rgba(0,0,0,0.05) !important; + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.03), + 0 1px 1px rgba(0, 0, 0, 0.05) !important; } .metric-filter-month { position: absolute; @@ -1212,7 +1313,9 @@ timestamp-badge[light-bg] { left: 42%; height: 35px; border-width: 0; - box-shadow: 0 1px 1px rgba(0,0,0,0.03), 0 1px 1px rgba(0,0,0,0.05) !important; + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.03), + 0 1px 1px rgba(0, 0, 0, 0.05) !important; } .metric-filter-day { position: absolute; @@ -1220,7 +1323,9 @@ timestamp-badge[light-bg] { left: 57%; height: 35px; border-width: 0; - box-shadow: 0 1px 1px rgba(0,0,0,0.03), 0 1px 1px rgba(0,0,0,0.05) !important; + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.03), + 0 1px 1px rgba(0, 0, 0, 0.05) !important; } .metric-summary-title { padding: 2px; @@ -1237,7 +1342,6 @@ timestamp-badge[light-bg] { margin-top: 10px; width: 40px !important; margin-left: 10px; - } .metric-summary-right { margin-left: 40px; @@ -1259,12 +1363,12 @@ timestamp-badge[light-bg] { width: 80px; margin-right: 5px; margin-top: -28px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.1), + 0 1px 1px rgba(0, 0, 0, 0.05); } .full-toggle-container { - height: 35px; - - + height: 35px; } .full-toggle-left { width: 50%; @@ -1317,7 +1421,7 @@ timestamp-badge[light-bg] { color: white; } .unit-toggle-container { - height: 35px; + height: 35px; } .unit-toggle-left { width: 50%; @@ -1374,7 +1478,7 @@ timestamp-badge[light-bg] { #no-border.item { border-width: 0 !important; } -#goal-signup-field{ +#goal-signup-field { width: 50%; margin-left: auto; margin-right: auto; @@ -1387,16 +1491,16 @@ timestamp-badge[light-bg] { opacity: 0.7; } .full-toggle-left:active { - opacity: 0.7; + opacity: 0.7; } .full-toggle-right:active { opacity: 0.7; } #iframe { - /*width: 375px !important;*/ - height: 100%; - -webkit-overflow-scrolling: touch !important; - overflow: scroll !important; + /*width: 375px !important;*/ + height: 100%; + -webkit-overflow-scrolling: touch !important; + overflow: scroll !important; } .buttons { @@ -1414,7 +1518,7 @@ timestamp-badge[light-bg] { .buttons input, .buttons select { text-align: center; - border: 1px solid rgb(20 20 20 / .2); + border: 1px solid rgb(20 20 20 / 0.2); border-radius: 10px; font-size: 13px; color: #222; @@ -1422,7 +1526,7 @@ timestamp-badge[light-bg] { min-width: 11ch; } -.buttons input[type="date"] { +.buttons input[type='date'] { color: transparent; } @@ -1440,15 +1544,14 @@ timestamp-badge[light-bg] { text-decoration: underline; } - .date-input-wrapper:after { - content: ""; + content: ''; position: absolute; top: 50%; translate: 0 -50%; right: 8px; font-size: 14px; - font-family: "Ionicons"; + font-family: 'Ionicons'; } .date-input-divider { diff --git a/www/i18n/en.json b/www/i18n/en.json index 4d9aea168..7f3798f16 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -1,464 +1,464 @@ { - "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" - }, + "trip-confirm": { + "services-please-fill-in": "Please fill in the {{text}} not listed.", + "services-cancel": "Cancel", + "services-save": "Save" + }, - "control":{ - "profile-tab": "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" - }, + "control": { + "profile-tab": "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" + }, - "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-logout": "Consent for data collection not found, please save your opcode, log out, and log back in with the same opcode. Note that you won't get any personalized stats until you do!", - "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 last updated on {{approval_date}}", - "consented-ok": "OK", - "qrcode": "My OPcode", - "qrcode-share-title": "You can save your OPcode to login easily in the future!" - }, + "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-logout": "Consent for data collection not found, please save your opcode, log out, and log back in with the same opcode. Note that you won't get any personalized stats until you do!", + "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 last updated on {{approval_date}}", + "consented-ok": "OK", + "qrcode": "My OPcode", + "qrcode-share-title": "You can save your OPcode to login easily in the future!" + }, - "metrics":{ - "dashboard-tab": "Dashboard", - "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": { - "label-tab": "Label", - "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": { + "dashboard-tab": "Dashboard", + "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" + }, - "multilabel":{ - "walk": "Walk", - "e-bike": "E-bike", - "bike": "Regular Bike", - "bikeshare": "Bikeshare", - "scootershare": "Scooter share", - "drove_alone": "Gas Car Drove Alone", - "shared_ride": "Gas Car Shared Ride", - "hybrid_drove_alone": "Hybrid Drove Alone", - "hybrid_shared_ride": "Hybrid Shared Ride", - "e_car_drove_alone": "E-Car Drove Alone", - "e_car_shared_ride": "E-Car Shared Ride", - "taxi": "Taxi / Uber / Lyft", - "bus": "Bus", - "train": "Train", - "free_shuttle": "Free Shuttle", - "air": "Air", - "not_a_trip": "Not a trip", - "no_travel": "No travel", - "home": "Home", - "work": "To Work", - "at_work": "At Work", - "school": "School", - "transit_transfer": "Transit transfer", - "shopping": "Shopping", - "meal": "Meal", - "pick_drop_person": "Pick-up / Drop off Person", - "pick_drop_item": "Pick-up / Drop off Item", - "personal_med": "Personal / Medical", - "access_recreation": "Access Recreation", - "exercise": "Recreation / Exercise", - "entertainment": "Entertainment / Social", - "religious": "Religious", - "other": "Other" - }, + "diary": { + "label-tab": "Label", + "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":{ - "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₂)" - }, + "multilabel": { + "walk": "Walk", + "e-bike": "E-bike", + "bike": "Regular Bike", + "bikeshare": "Bikeshare", + "scootershare": "Scooter share", + "drove_alone": "Gas Car Drove Alone", + "shared_ride": "Gas Car Shared Ride", + "hybrid_drove_alone": "Hybrid Drove Alone", + "hybrid_shared_ride": "Hybrid Shared Ride", + "e_car_drove_alone": "E-Car Drove Alone", + "e_car_shared_ride": "E-Car Shared Ride", + "taxi": "Taxi / Uber / Lyft", + "bus": "Bus", + "train": "Train", + "free_shuttle": "Free Shuttle", + "air": "Air", + "not_a_trip": "Not a trip", + "no_travel": "No travel", + "home": "Home", + "work": "To Work", + "at_work": "At Work", + "school": "School", + "transit_transfer": "Transit transfer", + "shopping": "Shopping", + "meal": "Meal", + "pick_drop_person": "Pick-up / Drop off Person", + "pick_drop_item": "Pick-up / Drop off Item", + "personal_med": "Personal / Medical", + "access_recreation": "Access Recreation", + "exercise": "Recreation / Exercise", + "entertainment": "Entertainment / Social", + "religious": "Religious", + "other": "Other" + }, - "details":{ - "speed": "Speed", - "time": "Time" - }, + "main-metrics": { + "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₂)" + }, - "list-datepicker-today": "Today", - "list-datepicker-close": "Close", - "list-datepicker-set": "Set", + "details": { + "speed": "Speed", + "time": "Time" + }, - "service":{ - "reading-server": "Reading from server...", - "reading-unprocessed-data": "Reading unprocessed data..." - }, + "list-datepicker-today": "Today", + "list-datepicker-close": "Close", + "list-datepicker-set": "Set", - "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" - } - }, + "service": { + "reading-server": "Reading from server...", + "reading-unprocessed-data": "Reading unprocessed data..." + }, - "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}}" + "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" + } + }, + + "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": { - "proceed": "Proceed", - "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": "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" + "intro": { + "proceed": "Proceed", + "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": "Please allow." + } }, - "allow_background": { - "samsung": "Disable 'Medium power saving mode'" + "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-app": "Welcome to {{appName}}!", + "app-name": "NREL OpenPATH", + "to-proceed-further": "To proceed further, please scan or paste an access code that has been provided by your program administrator through a website, email, text message, or printout.", + "code-hint": "The code begins with ‘nrelop’ and may be in barcode or text format.", + "scan-code": "Scan code", + "paste-code": "Paste code", + "scan-hint": "Scan the barcode with your phone camera", + "paste-hint": "Or, paste the code as text", + "about-app-title": "About {{appName}}", + "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.", + "tips-title": "Tip(s) for correct operation:", + "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" + }, + "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": { + "registration-check-token": "User registration error. Please check your token and try again.", + "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", + "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" }, - "consent":{ - "permissions" : "Permissions", - "button-accept": "I accept", - "button-decline": "I refuse" + "why": { + "header": "Why we collect this information" }, - "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" + "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." }, - "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." + "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." }, - "join": { - "welcome-to-app": "Welcome to {{appName}}!", - "app-name": "NREL OpenPATH", - "to-proceed-further": "To proceed further, please scan or paste an access code that has been provided by your program administrator through a website, email, text message, or printout.", - "code-hint": "The code begins with ‘nrelop’ and may be in barcode or text format.", - "scan-code": "Scan code", - "paste-code": "Paste code", - "scan-hint": "Scan the barcode with your phone camera", - "paste-hint": "Or, paste the code as text", - "about-app-title": "About {{appName}}", - "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.", - "tips-title": "Tip(s) for correct operation:", - "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" + "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." }, - "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" + "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." }, - "errors": { - "registration-check-token": "User registration error. Please check your token and try again.", - "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", - "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 " + "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-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}}." - } + "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/index.html b/www/index.html index 451c3047f..b46904cca 100644 --- a/www/index.html +++ b/www/index.html @@ -1,9 +1,13 @@ - + - - - + + + @@ -12,7 +16,7 @@ -
+
diff --git a/www/js/App.tsx b/www/js/App.tsx index b1806a5cc..ab4caebf7 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -7,24 +7,42 @@ import MetricsTab from './metrics/MetricsTab'; import ProfileSettings from './control/ProfileSettings'; import useAppConfig from './useAppConfig'; import OnboardingStack from './onboarding/OnboardingStack'; -import { OnboardingRoute, OnboardingState, getPendingOnboardingState } from './onboarding/onboardingHelper'; +import { + OnboardingRoute, + OnboardingState, + getPendingOnboardingState, +} from './onboarding/onboardingHelper'; import { setServerConnSettings } from './config/serverConn'; import AppStatusModal from './control/AppStatusModal'; import usePermissionStatus from './usePermissionStatus'; const defaultRoutes = (t) => [ - { key: 'label', title: t('diary.label-tab'), focusedIcon: 'check-bold', unfocusedIcon: 'check-outline' }, - { key: 'metrics', title: t('metrics.dashboard-tab'), focusedIcon: 'chart-box', unfocusedIcon: 'chart-box-outline' }, - { key: 'control', title: t('control.profile-tab'), focusedIcon: 'account', unfocusedIcon: 'account-outline' }, + { + key: 'label', + title: t('diary.label-tab'), + focusedIcon: 'check-bold', + unfocusedIcon: 'check-outline', + }, + { + key: 'metrics', + title: t('metrics.dashboard-tab'), + focusedIcon: 'chart-box', + unfocusedIcon: 'chart-box-outline', + }, + { + key: 'control', + title: t('control.profile-tab'), + focusedIcon: 'account', + unfocusedIcon: 'account-outline', + }, ]; export const AppContext = createContext({}); const App = () => { - const [index, setIndex] = useState(0); // will remain null while the onboarding state is still being determined - const [onboardingState, setOnboardingState] = useState(null); + const [onboardingState, setOnboardingState] = useState(null); const [permissionsPopupVis, setPermissionsPopupVis] = useState(false); const appConfig = useAppConfig(); const permissionStatus = usePermissionStatus(); @@ -33,7 +51,7 @@ const App = () => { const routes = useMemo(() => { const showMetrics = appConfig?.survey_info?.['trip-labels'] == 'MULTILABEL'; - return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter(r => r.key != 'metrics'); + return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter((r) => r.key != 'metrics'); }, [appConfig, t]); const renderScene = BottomNavigation.SceneMap({ @@ -43,7 +61,9 @@ const App = () => { }); const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); - useEffect(() => { refreshOnboardingState() }, []); + useEffect(() => { + refreshOnboardingState(); + }, []); useEffect(() => { if (!appConfig) return; @@ -54,17 +74,20 @@ const App = () => { const appContextValue = { appConfig, - onboardingState, setOnboardingState, refreshOnboardingState, + onboardingState, + setOnboardingState, + refreshOnboardingState, permissionStatus, - permissionsPopupVis, setPermissionsPopupVis, - } + permissionsPopupVis, + setPermissionsPopupVis, + }; console.debug('onboardingState in App', onboardingState); let appContent; if (onboardingState == null) { // if onboarding state is not yet determined, show a loading spinner - appContent = + appContent = ; } else if (onboardingState?.route == OnboardingRoute.DONE) { // if onboarding route is DONE, show the main app with navigation between tabs appContent = ( @@ -78,24 +101,27 @@ const App = () => { barStyle={{ height: 68, justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0)' }} // BottomNavigation uses secondaryContainer color for the background, but we want primaryContainer // (light blue), so we override here. - theme={{ colors: { secondaryContainer: colors.primaryContainer } }} /> + theme={{ colors: { secondaryContainer: colors.primaryContainer } }} + /> ); } else { // if there is an onboarding route that is not DONE, show the onboarding stack - appContent = + appContent = ; } - return (<> - - {appContent} + return ( + <> + + {appContent} - { /* If we are fully consented, (route > PROTOCOL), the permissions popup can show if needed. - This also includes if onboarding is DONE altogether (because "DONE" is > "PROTOCOL") */ } - {(onboardingState && onboardingState.route > OnboardingRoute.PROTOCOL) && - - } - - ); -} + {/* If we are fully consented, (route > PROTOCOL), the permissions popup can show if needed. + This also includes if onboarding is DONE altogether (because "DONE" is > "PROTOCOL") */} + {onboardingState && onboardingState.route > OnboardingRoute.PROTOCOL && ( + + )} + + + ); +}; export default App; diff --git a/www/js/angular-react-helper.tsx b/www/js/angular-react-helper.tsx index 4813ae2e9..3cf891666 100644 --- a/www/js/angular-react-helper.tsx +++ b/www/js/angular-react-helper.tsx @@ -15,11 +15,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/appTheme.ts b/www/js/appTheme.ts index a8660e811..641d1f680 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,23 +47,26 @@ 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) @@ -74,7 +77,7 @@ const flavorOverrides = { level1: '#e1e3e4', // lch(90 1 250) level2: '#d2d5d8', // lch(85 2 250) }, - } + }, }, } satisfies Record; @@ -83,7 +86,10 @@ 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 cb0db4bba..d0d63ebe7 100644 --- a/www/js/appstatus/ExplainPermissions.tsx +++ b/www/js/appstatus/ExplainPermissions.tsx @@ -1,41 +1,34 @@ -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; \ No newline at end of file +export default ExplainPermissions; diff --git a/www/js/appstatus/PermissionItem.tsx b/www/js/appstatus/PermissionItem.tsx index 2899943f1..cd111f3b3 100644 --- a/www/js/appstatus/PermissionItem.tsx +++ b/www/js/appstatus/PermissionItem.tsx @@ -1,21 +1,19 @@ -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; \ No newline at end of file + +export default PermissionItem; diff --git a/www/js/appstatus/PermissionsControls.tsx b/www/js/appstatus/PermissionsControls.tsx index 97ce7081a..39d5386d3 100644 --- a/www/js/appstatus/PermissionsControls.tsx +++ b/www/js/appstatus/PermissionsControls.tsx @@ -1,67 +1,63 @@ //component to view and manage permission settings -import React, { useContext, useState } from "react"; -import { StyleSheet, ScrollView, View } from "react-native"; +import React, { useContext, useState } from 'react'; +import { StyleSheet, ScrollView, View } from 'react-native'; import { Button, Text } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import PermissionItem from "./PermissionItem"; -import { refreshAllChecks } from "../usePermissionStatus"; -import ExplainPermissions from "./ExplainPermissions"; -import AlertBar from "../control/AlertBar"; -import { AppContext } from "../App"; +import { useTranslation } from 'react-i18next'; +import PermissionItem from './PermissionItem'; +import { refreshAllChecks } from '../usePermissionStatus'; +import ExplainPermissions from './ExplainPermissions'; +import AlertBar from '../control/AlertBar'; +import { AppContext } from '../App'; const PermissionsControls = ({ onAccept }) => { - const { t } = useTranslation(); - const [explainVis, setExplainVis] = useState(false); - const { permissionStatus } = useContext(AppContext); - const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = permissionStatus; + const { t } = useTranslation(); + const [explainVis, setExplainVis] = useState(false); + const { permissionStatus } = useContext(AppContext); + const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = + permissionStatus; - return ( - <> - {t('consent.permissions')} - - {t('intro.appstatus.overall-description')} - - - {checkList?.map((lc) => - - - )} - - - - - + return ( + <> + {t('consent.permissions')} + + {t('intro.appstatus.overall-description')} + + + {checkList?.map((lc) => )} + + + + + - - - ) -} + + + ); +}; const styles = StyleSheet.create({ - title: { - fontWeight: "bold", - fontSize: 22, - paddingBottom: 10 - }, - buttonBox: { - paddingHorizontal: 15, - paddingVertical: 10, - flexDirection: "row", - justifyContent: "space-evenly" - } - }); + title: { + fontWeight: 'bold', + fontSize: 22, + paddingBottom: 10, + }, + buttonBox: { + paddingHorizontal: 15, + paddingVertical: 10, + flexDirection: 'row', + justifyContent: 'space-evenly', + }, +}); export default PermissionsControls; diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index b9584a044..5f144888b 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -1,5 +1,5 @@ -import { DateTime } from "luxon"; -import { logDebug } from "./plugin/logger"; +import { DateTime } from 'luxon'; +import { logDebug } from './plugin/logger'; /** * @param url URL endpoint for the request @@ -19,8 +19,14 @@ export async function fetchUrlCached(url) { return text; } -export function getRawEntries(key_list, start_ts, end_ts, time_key = "metadata.write_ts", - max_entries = undefined, trunc_method = "sample") { +export function getRawEntries( + key_list, + start_ts, + end_ts, + time_key = 'metadata.write_ts', + max_entries = undefined, + trunc_method = 'sample', +) { return new Promise((rs, rj) => { const msgFiller = (message) => { message.key_list = key_list; @@ -32,18 +38,29 @@ export function getRawEntries(key_list, start_ts, end_ts, time_key = "metadata.w message.trunc_method = trunc_method; } logDebug(`About to return message ${JSON.stringify(message)}`); - } - logDebug("getRawEntries: about to get pushGetJSON for the timestamp"); - window['cordova'].plugins.BEMServerComm.pushGetJSON("/datastreams/find_entries/timestamp", msgFiller, rs, rj); - }).catch(error => { + }; + logDebug('getRawEntries: about to get pushGetJSON for the timestamp'); + window['cordova'].plugins.BEMServerComm.pushGetJSON( + '/datastreams/find_entries/timestamp', + msgFiller, + rs, + rj, + ); + }).catch((error) => { error = `While getting raw entries, ${error}`; - throw(error); + throw error; }); } // time_key is typically metadata.write_ts or data.ts -export function getRawEntriesForLocalDate(key_list, start_ts, end_ts, time_key = "metadata.write_ts", - max_entries = undefined, trunc_method = "sample") { +export function getRawEntriesForLocalDate( + key_list, + start_ts, + end_ts, + time_key = 'metadata.write_ts', + max_entries = undefined, + trunc_method = 'sample', +) { return new Promise((rs, rj) => { const msgFiller = (message) => { message.key_list = key_list; @@ -54,105 +71,137 @@ export function getRawEntriesForLocalDate(key_list, start_ts, end_ts, time_key = message.max_entries = max_entries; message.trunc_method = trunc_method; } - logDebug("About to return message " + JSON.stringify(message)); + logDebug('About to return message ' + JSON.stringify(message)); }; - logDebug("getRawEntries: about to get pushGetJSON for the timestamp"); - window['cordova'].plugins.BEMServerComm.pushGetJSON("/datastreams/find_entries/local_date", msgFiller, rs, rj); - }).catch(error => { - error = "While getting raw entries for local date, " + error; - throw (error); + logDebug('getRawEntries: about to get pushGetJSON for the timestamp'); + window['cordova'].plugins.BEMServerComm.pushGetJSON( + '/datastreams/find_entries/local_date', + msgFiller, + rs, + rj, + ); + }).catch((error) => { + error = 'While getting raw entries for local date, ' + error; + throw error; }); -}; +} export function getPipelineRangeTs() { return new Promise((rs, rj) => { - logDebug("getting pipeline range timestamps"); - window['cordova'].plugins.BEMServerComm.getUserPersonalData("/pipeline/get_range_ts", rs, rj); - }).catch(error => { + logDebug('getting pipeline range timestamps'); + window['cordova'].plugins.BEMServerComm.getUserPersonalData('/pipeline/get_range_ts', rs, rj); + }).catch((error) => { error = `While getting pipeline range timestamps, ${error}`; - throw(error); + throw error; }); } export function getPipelineCompleteTs() { return new Promise((rs, rj) => { - logDebug("getting pipeline complete timestamp"); - window['cordova'].plugins.BEMServerComm.getUserPersonalData("/pipeline/get_complete_ts", rs, rj); - }).catch(error => { + logDebug('getting pipeline complete timestamp'); + window['cordova'].plugins.BEMServerComm.getUserPersonalData( + '/pipeline/get_complete_ts', + rs, + rj, + ); + }).catch((error) => { error = `While getting pipeline complete timestamp, ${error}`; - throw(error); + throw error; }); } -export function getMetrics(timeType: 'timestamp'|'local_date', metricsQuery) { +export function getMetrics(timeType: 'timestamp' | 'local_date', metricsQuery) { return new Promise((rs, rj) => { const msgFiller = (message) => { for (let key in metricsQuery) { message[key] = metricsQuery[key]; } - } - window['cordova'].plugins.BEMServerComm.pushGetJSON(`/result/metrics/${timeType}`, msgFiller, rs, rj); - }).catch(error => { + }; + window['cordova'].plugins.BEMServerComm.pushGetJSON( + `/result/metrics/${timeType}`, + msgFiller, + rs, + rj, + ); + }).catch((error) => { error = `While getting metrics, ${error}`; - throw(error); + throw error; }); } export function getAggregateData(path: string, data: any) { return new Promise((rs, rj) => { const fullUrl = `${window['$rootScope'].connectUrl}/${path}`; - data["aggregate"] = true; + data['aggregate'] = true; - if (window['$rootScope'].aggregateAuth === "no_auth") { - logDebug(`getting aggregate data without user authentication from ${fullUrl} with arguments ${JSON.stringify(data)}`); + if (window['$rootScope'].aggregateAuth === 'no_auth') { + logDebug( + `getting aggregate data without user authentication from ${fullUrl} with arguments ${JSON.stringify( + data, + )}`, + ); const options = { method: 'post', data: data, - responseType: 'json' - } - window['cordova'].plugin.http.sendRequest(fullUrl, options, + responseType: 'json', + }; + window['cordova'].plugin.http.sendRequest( + fullUrl, + options, (response) => { rs(response.data); - }, (error) => { + }, + (error) => { rj(error); - }); + }, + ); } else { - logDebug(`getting aggregate data with user authentication from ${fullUrl} with arguments ${JSON.stringify(data)}`); + logDebug( + `getting aggregate data with user authentication from ${fullUrl} with arguments ${JSON.stringify( + data, + )}`, + ); const msgFiller = (message) => { return Object.assign(message, data); - } + }; window['cordova'].plugins.BEMServerComm.pushGetJSON(`/${path}`, msgFiller, rs, rj); } - }).catch(error => { + }).catch((error) => { error = `While getting aggregate data, ${error}`; - throw(error); + throw error; }); } export function registerUser() { return new Promise((rs, rj) => { - window['cordova'].plugins.BEMServerComm.getUserPersonalData("/profile/create", rs, rj); - }).catch(error => { + window['cordova'].plugins.BEMServerComm.getUserPersonalData('/profile/create', rs, rj); + }).catch((error) => { error = `While registering user, ${error}`; - throw(error); + throw error; }); } export function updateUser(updateDoc) { return new Promise((rs, rj) => { - window['cordova'].plugins.BEMServerComm.postUserPersonalData("/profile/update", "update_doc", updateDoc, rs, rj); - }).catch(error => { + window['cordova'].plugins.BEMServerComm.postUserPersonalData( + '/profile/update', + 'update_doc', + updateDoc, + rs, + rj, + ); + }).catch((error) => { error = `While updating user, ${error}`; - throw(error); + throw error; }); } export function getUser() { return new Promise((rs, rj) => { - window['cordova'].plugins.BEMServerComm.getUserPersonalData("/profile/get", rs, rj); - }).catch(error => { + window['cordova'].plugins.BEMServerComm.getUserPersonalData('/profile/get', rs, rj); + }).catch((error) => { error = `While getting user, ${error}`; - throw(error); + throw error; }); } @@ -162,15 +211,21 @@ export function putOne(key, data) { write_ts: nowTs, read_ts: nowTs, time_zone: DateTime.local().zoneName, - type: "message", + type: 'message', key: key, platform: window['device'].platform, }; const entryToPut = { metadata, data }; return new Promise((rs, rj) => { - window['cordova'].plugins.BEMServerComm.postUserPersonalData("/usercache/putone", "the_entry", entryToPut, rs, rj); - }).catch(error => { - error = "While putting one entry, " + error; - throw(error); + window['cordova'].plugins.BEMServerComm.postUserPersonalData( + '/usercache/putone', + 'the_entry', + entryToPut, + rs, + rj, + ); + }).catch((error) => { + error = 'While putting one entry, ' + error; + throw error; }); -}; +} diff --git a/www/js/components/ActionMenu.tsx b/www/js/components/ActionMenu.tsx index 296717a00..0693acc8b 100644 --- a/www/js/components/ActionMenu.tsx +++ b/www/js/components/ActionMenu.tsx @@ -1,41 +1,44 @@ -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 ActionMenu = ({ vis, setVis, title, actionSet, onAction, onExit }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); - const { t } = useTranslation(); - const { colors } = useTheme(); + return ( + setVis(false)} transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {title} + + {actionSet?.map((e) => ( + + ))} + + + + + + + ); +}; - 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 +export default ActionMenu; diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 1e957923b..ccf1a6f74 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,13 +1,12 @@ -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) { @@ -15,13 +14,11 @@ 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 28a31ff6a..92febb32b 100644 --- a/www/js/components/Carousel.tsx +++ b/www/js/components/Carousel.tsx @@ -2,26 +2,27 @@ 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 = { @@ -31,8 +32,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 79c6e40e4..d7687e424 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -1,4 +1,3 @@ - import React, { useEffect, useRef, useState, useMemo } from 'react'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; @@ -9,48 +8,62 @@ 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], -.5) || getColorForChartEl(chartRef.current, e, barCtx, 'border') - ), + 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'), borderWidth: borderWidth || 2, borderRadius: 3, })), @@ -60,14 +73,16 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, // 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({ @@ -80,11 +95,15 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, 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; + ...(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; + }, + font: { size: 11 }, // default is 12, we want a tad smaller + }, + reverse: true, + 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; + x: { + title: { display: true, text: axisTitle }, + stacked, }, - }, - stacked, - }, - y: { - 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 16c716f93..6a04cb079 100644 --- a/www/js/components/DiaryButton.tsx +++ b/www/js/components/DiaryButton.tsx @@ -1,28 +1,25 @@ -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"; - -type Props = ButtonProps & { fillColor?: string, borderColor?: string }; -const DiaryButton = ({ children, fillColor, borderColor, icon, ...rest } : Props) => { +import { Icon } from './Icon'; +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 ( - @@ -51,7 +48,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 0b4c7253e..3d13d0996 100644 --- a/www/js/components/Icon.tsx +++ b/www/js/components/Icon.tsx @@ -7,14 +7,19 @@ 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.tsx b/www/js/components/LeafletView.tsx index cf26cb933..b0b60912b 100644 --- a/www/js/components/LeafletView.tsx +++ b/www/js/components/LeafletView.tsx @@ -1,15 +1,14 @@ -import React, { useEffect, useRef, useState } from "react"; -import { View } from "react-native"; -import { useTheme } from "react-native-paper"; -import L from "leaflet"; +import React, { useEffect, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { useTheme } from 'react-native-paper'; +import L from 'leaflet'; 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 +22,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 +45,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 +53,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); } }; diff --git a/www/js/components/LineChart.tsx b/www/js/components/LineChart.tsx index 66d21aac2..456642a63 100644 --- a/www/js/components/LineChart.tsx +++ b/www/js/components/LineChart.tsx @@ -1,11 +1,9 @@ -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 7e9cb1217..294015152 100644 --- a/www/js/components/NavBarButton.tsx +++ b/www/js/components/NavBarButton.tsx @@ -1,31 +1,39 @@ -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(.07).rgb().string(); - const outlineColor = color(colors.onBackground).alpha(.2).rgb().string(); + const buttonColor = color(colors.onBackground).alpha(0.07).rgb().string(); + const outlineColor = color(colors.onBackground).alpha(0.2).rgb().string(); - return (<> - - ); + return ( + <> + + + ); }; export const s = StyleSheet.create({ diff --git a/www/js/components/QrCode.tsx b/www/js/components/QrCode.tsx index 74c66863f..83498f5da 100644 --- a/www/js/components/QrCode.tsx +++ b/www/js/components/QrCode.tsx @@ -2,45 +2,56 @@ 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 QRCode from "react-qr-code"; +import React from 'react'; +import QRCode from 'react-qr-code'; export function shareQR(message) { /*code adapted from demo of react-qr-code*/ - const svg = document.querySelector(".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"); + 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"); + const pngFile = canvas.toDataURL('image/png'); var prepopulateQRMessage = {}; prepopulateQRMessage['files'] = [pngFile]; prepopulateQRMessage['url'] = message; prepopulateQRMessage['message'] = message; //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); - }); - } + 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 QrCode = ({ value, ...rest }) => { - let hasLink = value.toString().includes("//"); - if(!hasLink) { - value = "emission://login_token?token=" + value; + let hasLink = value.toString().includes('//'); + if (!hasLink) { + value = 'emission://login_token?token=' + value; } - - return ; + + return ( + + ); }; export default QrCode; diff --git a/www/js/components/ToggleSwitch.tsx b/www/js/components/ToggleSwitch.tsx index 5fdf1cc46..7f753a9a0 100644 --- a/www/js/components/ToggleSwitch.tsx +++ b/www/js/components/ToggleSwitch.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { SegmentedButtons, SegmentedButtonsProps, useTheme } from "react-native-paper"; - -const ToggleSwitch = ({ value, buttons, ...rest}: SegmentedButtonsProps) => { +import { SegmentedButtons, SegmentedButtonsProps, useTheme } from 'react-native-paper'; +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, @@ -18,9 +19,11 @@ 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 f0da14619..77490f7ff 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -15,15 +15,23 @@ 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; @@ -41,11 +49,11 @@ export function getChartHeight(chartDatasets, numVisibleDatasets, indexAxis, isH 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; @@ -54,27 +62,34 @@ 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; @@ -95,7 +110,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; @@ -104,19 +119,26 @@ 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; } @@ -129,9 +151,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 * .5)).hex(); + return colorObj.darken(Math.abs(change * 0.5)).hex(); } else { return colorObj.lighten(Math.abs(change)).hex(); } @@ -150,7 +172,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]) { @@ -158,4 +180,4 @@ export const dedupColors = (colors: string[][]) => { } } return dedupedColors; -} +}; diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index 6d9b2b372..9c28958ac 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -1,55 +1,59 @@ -import i18next from "i18next"; -import { displayError, logDebug, logWarn } from "../plugin/logger"; -import { getAngularService } from "../angular-react-helper"; -import { fetchUrlCached } from "../commHelper"; -import { storageClear, storageGet, storageSet } from "../plugin/storage"; +import i18next from 'i18next'; +import { displayError, logDebug, logWarn } from '../plugin/logger'; +import { getAngularService } from '../angular-react-helper'; +import { fetchUrlCached } from '../commHelper'; +import { storageClear, storageGet, storageSet } from '../plugin/storage'; -export const CONFIG_PHONE_UI="config/app_ui_config"; -export const CONFIG_PHONE_UI_KVSTORE ="CONFIG_PHONE_UI"; +export const CONFIG_PHONE_UI = 'config/app_ui_config'; +export const CONFIG_PHONE_UI_KVSTORE = 'CONFIG_PHONE_UI'; export let storedConfig = null; export let configChanged = false; -export const setConfigChanged = (b) => configChanged = b; +export const setConfigChanged = (b) => (configChanged = b); 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 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) { if (!config.name) { if (config.server) { config.name = _getStudyName(config.server.connectUrl); } else { - config.name = "dev"; + config.name = 'dev'; } } -} +}; 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" - } - } + 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', + }; } -} +}; /* 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. @@ -58,54 +62,57 @@ const _backwardsCompatSurveyFill = function (config) { 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')); + 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); - logDebug("Successfully found config, result is " + JSON.stringify(config).substring(0, 10)); + logDebug('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); - const connectionURL = config.server ? config.server.connectUrl : "dev defaults"; + const connectionURL = config.server ? config.server.connectUrl : 'dev defaults'; _fillStudyName(config); _backwardsCompatSurveyFill(config); - logDebug("Successfully downloaded config with version " + config.version - + " for " + config.intro.translated_text.en.deployment_name - + " and data collection URL " + connectionURL); + logDebug( + '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) => { - logDebug("Received request to join " + label); + logDebug('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) { - logDebug("Fetching config from github"); + logDebug('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 { - logDebug("Running in dev environment, checking for locally hosted config"); + } else { + logDebug('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) { - logDebug("Local config not found"); + logDebug('Local config not found'); return fetchConfig(label, true); } } -} +}; /* * We want to support both old style and new style tokens. @@ -120,12 +127,12 @@ const fetchConfig = async (label, alreadyTriedLocal = false) => { * So let's support two separate functions here - extractStudyName and extractSubgroup */ function extractStudyName(token) { - const tokenParts = token.split("_"); + 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") { + if (tokenParts[0] != 'nrelop') { throw new Error(i18next.t('config.no-nrelop-start', { token: token })); } return tokenParts[1]; @@ -134,20 +141,27 @@ function extractStudyName(token) { function extractSubgroup(token, config) { if (config.opcode) { // new style study, expects token with sub-group - const tokenParts = token.split("_"); - if (tokenParts.length <= 3) { // no subpart defined + 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 })); + 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); + console.log('subgroup ' + tokenParts[2] + ' found in list ' + config.opcode.subgroups); return tokenParts[2]; } } else { - if (tokenParts[2] != "default") { + if (tokenParts[2] != 'default') { // subpart not in config list throw new Error(i18next.t('config.invalid-subgroup-no-default', { token: token })); } else { @@ -161,59 +175,66 @@ function extractSubgroup(token, config) { * 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..."); + */ + console.log('Old-style study, expecting token without a subgroup...'); return undefined; } } /** -* loadNewConfig download and load a new config from the server if it is a differ -* @param {[]} newToken the new token, which includes parts for the study label, subgroup, and user -* @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 -*/ + * loadNewConfig download and load a new config from the server if it is a differ + * @param {[]} newToken the new token, which includes parts for the study label, subgroup, and user + * @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 + */ function loadNewConfig(newToken, existingVersion = null) { const newStudyLabel = extractStudyName(newToken); - return readConfigFromServer(newStudyLabel).then((downloadedConfig) => { - if (downloadedConfig.version == existingVersion) { - logDebug("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 = extractSubgroup(newToken, downloadedConfig); - const toSaveConfig = { - ...downloadedConfig, - joined: { opcode: newToken, study_name: newStudyLabel, subgroup: subgroup } - } - const storeConfigPromise = window['cordova'].plugins.BEMUserCache.putRWDocument( - CONFIG_PHONE_UI, toSaveConfig); - const storeInKVStorePromise = storageSet(CONFIG_PHONE_UI_KVSTORE, toSaveConfig); - logDebug("UI_CONFIG: about to store " + JSON.stringify(toSaveConfig)); - // loaded new config, so it is both ready and changed - return Promise.all([storeConfigPromise, storeInKVStorePromise]).then( - ([result, kvStoreResult]) => { - logDebug("UI_CONFIG: Stored dynamic config in KVStore successfully, result = " + JSON.stringify(kvStoreResult)); - storedConfig = toSaveConfig; - configChanged = true; - return true; - }).catch((storeError) => - displayError(storeError, i18next.t('config.unable-to-store-config')) + return readConfigFromServer(newStudyLabel) + .then((downloadedConfig) => { + if (downloadedConfig.version == existingVersion) { + logDebug('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 = extractSubgroup(newToken, downloadedConfig); + const toSaveConfig = { + ...downloadedConfig, + joined: { opcode: newToken, study_name: newStudyLabel, subgroup: subgroup }, + }; + const storeConfigPromise = window['cordova'].plugins.BEMUserCache.putRWDocument( + CONFIG_PHONE_UI, + toSaveConfig, ); - }).catch((fetchErr) => { - displayError(fetchErr, i18next.t('config.unable-download-config')); - }); + const storeInKVStorePromise = storageSet(CONFIG_PHONE_UI_KVSTORE, toSaveConfig); + logDebug('UI_CONFIG: about to store ' + JSON.stringify(toSaveConfig)); + // loaded new config, so it is both ready and changed + return Promise.all([storeConfigPromise, storeInKVStorePromise]) + .then(([result, kvStoreResult]) => { + logDebug( + 'UI_CONFIG: Stored dynamic config in KVStore successfully, result = ' + + JSON.stringify(kvStoreResult), + ); + storedConfig = toSaveConfig; + configChanged = true; + return true; + }) + .catch((storeError) => + displayError(storeError, i18next.t('config.unable-to-store-config')), + ); + }) + .catch((fetchErr) => { + displayError(fetchErr, i18next.t('config.unable-download-config')); + }); } export function initByUser(urlComponents) { const { token } = urlComponents; try { - return loadNewConfig(token) - .catch((fetchErr) => { - displayError(fetchErr, i18next.t('config.unable-download-config')); - }); + return loadNewConfig(token).catch((fetchErr) => { + displayError(fetchErr, i18next.t('config.unable-download-config')); + }); } catch (error) { displayError(error, i18next.t('config.invalid-opcode-format')); return Promise.reject(error); @@ -229,19 +250,21 @@ export function getConfig() { if (storedConfig) return Promise.resolve(storedConfig); return storageGet(CONFIG_PHONE_UI_KVSTORE).then((config) => { if (config && Object.keys(config).length) { - logDebug("Got config from KVStore: " + JSON.stringify(config)); + logDebug('Got config from KVStore: ' + JSON.stringify(config)); storedConfig = config; return config; } - logDebug("No config found in KVStore, fetching from native storage"); - return window['cordova'].plugins.BEMUserCache.getDocument(CONFIG_PHONE_UI, false).then((config) => { - if (config && Object.keys(config).length) { - logDebug("Got config from native storage: " + JSON.stringify(config)); - storedConfig = config; - return config; - } - logWarn("No config found in native storage either. Returning null"); - return null; - }); + logDebug('No config found in KVStore, fetching from native storage'); + return window['cordova'].plugins.BEMUserCache.getDocument(CONFIG_PHONE_UI, false).then( + (config) => { + if (config && Object.keys(config).length) { + logDebug('Got config from native storage: ' + JSON.stringify(config)); + storedConfig = config; + return config; + } + logWarn('No config found in native storage either. Returning null'); + return null; + }, + ); }); } diff --git a/www/js/config/enketo-config.js b/www/js/config/enketo-config.js index 00ea6f4be..07ac599c2 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/serverConn.ts b/www/js/config/serverConn.ts index e3371270b..b0850974e 100644 --- a/www/js/config/serverConn.ts +++ b/www/js/config/serverConn.ts @@ -1,13 +1,14 @@ -import { logDebug } from "../plugin/logger"; +import { logDebug } from '../plugin/logger'; export async function setServerConnSettings(config) { if (!config) return Promise.resolve(null); if (config.server) { - logDebug("connectionConfig = " + JSON.stringify(config.server)); + logDebug('connectionConfig = ' + JSON.stringify(config.server)); return window['cordova'].plugins.BEMConnectionSettings.setSettings(config.server); } else { - const defaultConfig = await window['cordova'].plugins.BEMConnectionSettings.getDefaultSettings(); - logDebug("defaultConfig = " + JSON.stringify(defaultConfig)); + const defaultConfig = + await window['cordova'].plugins.BEMConnectionSettings.getDefaultSettings(); + logDebug('defaultConfig = ' + JSON.stringify(defaultConfig)); return window['cordova'].plugins.BEMConnectionSettings.setSettings(defaultConfig); } } diff --git a/www/js/config/useImperialConfig.ts b/www/js/config/useImperialConfig.ts index a9680048c..1b7e2c346 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,26 +15,21 @@ 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); -} +}; export 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; -} +}; export 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 = useAppConfig(); @@ -46,11 +41,13 @@ export function useImperialConfig() { }, [appConfig]); 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 fbac80056..c86401b03 100644 --- a/www/js/control/AlertBar.jsx +++ b/www/js/control/AlertBar.jsx @@ -1,38 +1,37 @@ -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; \ No newline at end of file + ); +}; + +export default AlertBar; diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index e7f5aa97b..8666f9ccf 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -1,40 +1,44 @@ -import React, { useContext, useEffect } from "react"; -import { Modal, useWindowDimensions } from "react-native"; +import React, { useContext, useEffect } from 'react'; +import { Modal, useWindowDimensions } from 'react-native'; import { Dialog, useTheme } from 'react-native-paper'; -import PermissionsControls from "../appstatus/PermissionsControls"; -import { settingStyles } from "./ProfileSettings"; -import { AppContext } from "../App"; +import PermissionsControls from '../appstatus/PermissionsControls'; +import { settingStyles } from './ProfileSettings'; +import { AppContext } from '../App'; //TODO -- import settings styles for dialog const AppStatusModal = ({ permitVis, setPermitVis }) => { - const { height: windowHeight } = useWindowDimensions(); - const { permissionStatus } = useContext(AppContext); - const { overallStatus, checkList } = permissionStatus; - const { colors } = useTheme(); + const { height: windowHeight } = useWindowDimensions(); + const { permissionStatus } = useContext(AppContext); + const { overallStatus, checkList } = permissionStatus; + const { colors } = useTheme(); - /* Listen for permissions status changes to determine if we should show the modal. */ - useEffect(() => { - if (overallStatus === false) { - setPermitVis(true); - } + /* Listen for permissions status changes to determine if we should show the modal. */ + useEffect(() => { + if (overallStatus === false) { + setPermitVis(true); + } }, [overallStatus, checkList]); - return ( - { - if(overallStatus){(setPermitVis(false))} - }} - transparent={true}> - setPermitVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - - setPermitVis(false)}> - - - - - ) -} + return ( + { + if (overallStatus) { + setPermitVis(false); + } + }} + transparent={true}> + setPermitVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + + setPermitVis(false)}> + + + + ); +}; export default AppStatusModal; diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index d93c498a9..cc3efa8c1 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -1,284 +1,362 @@ -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 = await forceTransitionWrapper(transition); - window.alert('success -> '+result); - } catch (err) { - window.alert('error -> '+err); - console.log("error forcing state", err); - } + try { + let result = await 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 + 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'; } 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"; - } else { - window.alert("Emission does not support this platform"); - } + 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"]; - } - } else { - if (window['cordova'].platformId == 'ios') { - tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyHundredMeters"]; - } else if (window['cordova'].platformId == 'android') { - tempConfig.accuracy = accuracyOptions["PRIORITY_BALANCED_POWER_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']; } - try{ - let set = await setConfig(tempConfig); - console.log("setConfig Sucess"); - } catch (err) { - Logger.displayError("Error while setting collection config", err); + } 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); + } } /* -* 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}); - } - } - } else { - retVal.push({'key': prop, 'val': config[prop]}); +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] }); } - return retVal; -} + } + return retVal; +}; const ControlCollectionHelper = ({ editVis, setEditVis }) => { - const {colors} = useTheme(); - const Logger = getAngularService("Logger"); + const { colors } = useTheme(); + const Logger = getAngularService('Logger'); - 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); - } + const [localConfig, setLocalConfig] = useState(); + const [accuracyActions, setAccuracyActions] = useState([]); + const [accuracyVis, setAccuracyVis] = useState(false); - useEffect(() => { - getCollectionSettings(); - }, [editVis]) + 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]); - const formatAccuracyForActions = function(accuracyOptions) { - let tempAccuracyActions = []; - for (var name in accuracyOptions) { - tempAccuracyActions.push({text: name, value: accuracyOptions[name]}); - } - return tempAccuracyActions; + const formatAccuracyForActions = function (accuracyOptions) { + let tempAccuracyActions = []; + for (var name in accuracyOptions) { + tempAccuracyActions.push({ text: name, value: accuracyOptions[name] }); } + 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); - setEditVis(false); - } catch(err) { - Logger.displayError("Error while setting collection config", err); - } + async function saveAndReload() { + console.log('new config = ', localConfig); + try { + let set = await setConfig(localConfig); + setEditVis(false); + } 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")}/> - - } + + ); + } + 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} - - - - - - - + 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 ControlCollectionHelper; diff --git a/www/js/control/ControlDataTable.jsx b/www/js/control/ControlDataTable.jsx index 796b057ec..932762400 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 edc0e7470..7fdf3fa37 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -1,284 +1,317 @@ -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 { addStatEvent, statKeys } from "../plugin/clientStats"; -import { updateUser } from "../commHelper"; +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 { addStatEvent, statKeys } from '../plugin/clientStats'; +import { updateUser } from '../commHelper'; /* -* 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 Logger = getAngularService('Logger'); - - const [dataPendingVis, setDataPendingVis] = useState(false); - const [dataPushedVis, setDataPushedVis] = useState(false); - - async function forceSync() { - try { - let addedEvent = addStatEvent(statKeys.BUTTON_FORCE_SYNC); - console.log("Added "+statKeys.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"; - } +export const ForceSyncRow = ({ getState }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const Logger = getAngularService('Logger'); + + const [dataPendingVis, setDataPendingVis] = useState(false); + const [dataPushedVis, setDataPushedVis] = useState(false); + + async function forceSync() { + try { + let addedEvent = addStatEvent(statKeys.BUTTON_FORCE_SYNC); + console.log('Added ' + statKeys.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 getEndTransitionKey = function() { - if(window.cordova.platformId == 'android') { - return "local.transition.stopped_moving"; - } - else if(window.cordova.platformId == 'ios') { - return "T_TRIP_ENDED"; - } + const getStartTransitionKey = function () { + if (window.cordova.platformId == 'android') { + return 'local.transition.exited_geofence'; + } else if (window.cordova.platformId == 'ios') { + return 'T_EXITED_GEOFENCE'; } + }; - const getOngoingTransitionState = function() { - if(window.cordova.platformId == 'android') { - return "local.state.ongoing_trip"; - } - else if(window.cordova.platformId == 'ios') { - return "STATE_ONGOING_TRIP"; - } + const getEndTransitionKey = function () { + if (window.cordova.platformId == 'android') { + return 'local.transition.stopped_moving'; + } else if (window.cordova.platformId == 'ios') { + return 'T_TRIP_ENDED'; } + }; - 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; + 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 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 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; + } + + 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 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); - } + const { t } = useTranslation(); + const { colors } = useTheme(); + const Logger = getAngularService('Logger'); - 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!! - 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 [localConfig, setLocalConfig] = useState(); + const [intervalVis, setIntervalVis] = useState(false); - const onChooseInterval = function(interval) { - let tempConfig = {...localConfig}; - tempConfig.sync_interval = interval.value; - setLocalConfig(tempConfig); - } + /* + * Functions to read and format values for display + */ + async function getSyncSettings() { + let tempConfig = await getConfig(); + setLocalConfig(tempConfig); + } + + useEffect(() => { + getSyncSettings(); + }, [editVis]); - const onTogglePush = function() { - let tempConfig = {...localConfig}; - tempConfig.ios_use_remote_push = !localConfig.ios_use_remote_push; - setLocalConfig(tempConfig); + 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!! + 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); } + } - /* - * 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} - - - - - - - - - {}}> - - ); + 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); + }; + + /* + * 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} + + + + + + + + + {}}> + + ); +}; + export default ControlSyncHelper; diff --git a/www/js/control/DataDatePicker.tsx b/www/js/control/DataDatePicker.tsx index 83e0986b2..7f143f3bd 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; \ No newline at end of file +export default DataDatePicker; diff --git a/www/js/control/DemographicsSettingRow.jsx b/www/js/control/DemographicsSettingRow.jsx index be02dd6d3..c8a0a7297 100644 --- a/www/js/control/DemographicsSettingRow.jsx +++ b/www/js/control/DemographicsSettingRow.jsx @@ -1,13 +1,12 @@ -import React, { useState } from "react"; -import SettingRow from "./SettingRow"; -import { loadPreviousResponseForSurvey } from "../survey/enketo/enketoHelper"; -import EnketoModal from "../survey/enketo/EnketoModal"; +import React, { useState } from 'react'; +import SettingRow from './SettingRow'; +import { loadPreviousResponseForSurvey } from '../survey/enketo/enketoHelper'; +import EnketoModal from '../survey/enketo/EnketoModal'; -export const DEMOGRAPHIC_SURVEY_NAME = "UserProfileSurvey"; -export const DEMOGRAPHIC_SURVEY_DATAKEY = "manual/demographic_survey"; - -const DemographicsSettingRow = ({ }) => { +export const DEMOGRAPHIC_SURVEY_NAME = 'UserProfileSurvey'; +export const DEMOGRAPHIC_SURVEY_DATAKEY = 'manual/demographic_survey'; +const DemographicsSettingRow = ({}) => { const [surveyModalVisible, setSurveyModalVisible] = useState(false); const [prevSurveyResponse, setPrevSurveyResponse] = useState(null); @@ -20,16 +19,26 @@ const DemographicsSettingRow = ({ }) => { }); } - return (<> - - setSurveyModalVisible(false)} - onResponseSaved={() => setSurveyModalVisible(false)} surveyName={DEMOGRAPHIC_SURVEY_NAME} - opts={{ - prefilledSurveyResponse: prevSurveyResponse, - dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, - }} /> - ); + return ( + <> + + setSurveyModalVisible(false)} + onResponseSaved={() => setSurveyModalVisible(false)} + surveyName={DEMOGRAPHIC_SURVEY_NAME} + opts={{ + prefilledSurveyResponse: prevSurveyResponse, + dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, + }} + /> + + ); }; export default DemographicsSettingRow; diff --git a/www/js/control/ExpandMenu.jsx b/www/js/control/ExpandMenu.jsx index 2f8bb8ef1..65c2fb3b3 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; \ No newline at end of file +export default ExpansionSection; diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index e33d2f9a3..ad369fbff 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -1,153 +1,183 @@ -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(); + } + + 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); } - - 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); - } - setEntries([...entries].concat(tempEntries)); //push the new entries onto the list - setLoadStats(tempLoadStats); + } + + 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 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()}}} - /> - - - - - - ); + 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 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 LogPage; diff --git a/www/js/control/PopOpCode.jsx b/www/js/control/PopOpCode.jsx index 21ce227c0..510ee84fd 100644 --- a/www/js/control/PopOpCode.jsx +++ b/www/js/control/PopOpCode.jsx @@ -1,79 +1,92 @@ -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}) => { - const { t } = useTranslation(); - const { colors } = useTheme(); +const PopOpCode = ({ visibilityValue, tokenURL, action, setVis }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); - const opcodeList = tokenURL.split("="); - const opcode = opcodeList[opcodeList.length - 1]; - - const [copyAlertVis, setCopyAlertVis] = useState(false); + const opcodeList = tokenURL.split('='); + const opcode = opcodeList[opcodeList.length - 1]; - const copyText = function(textToCopy){ - navigator.clipboard.writeText(textToCopy).then(() => { - setCopyAlertvis(true); - }) - } + const [copyAlertVis, setCopyAlertVis] = useState(false); - let copyButton; - if (window.cordova.platformId == "ios"){ - copyButton = {copyText(opcode); setCopyAlertVis(true)}} style={styles.button}/> - } + const copyText = function (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(() => { + setCopyAlertvis(true); + }); + }; - 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} - - - - + 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} + + + + + + + + ); +}; 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; \ No newline at end of file +export default PopOpCode; diff --git a/www/js/control/PrivacyPolicyModal.tsx b/www/js/control/PrivacyPolicyModal.tsx index 7a67426ac..27cb907dd 100644 --- a/www/js/control/PrivacyPolicyModal.tsx +++ b/www/js/control/PrivacyPolicyModal.tsx @@ -1,35 +1,34 @@ -import React from "react"; -import { Modal, useWindowDimensions, ScrollView } from "react-native"; +import React from 'react'; +import { Modal, useWindowDimensions, ScrollView } from 'react-native'; import { Dialog, Button, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import PrivacyPolicy from "../onboarding/PrivacyPolicy"; -import { settingStyles } from "./ProfileSettings"; +import { useTranslation } from 'react-i18next'; +import PrivacyPolicy from '../onboarding/PrivacyPolicy'; +import { settingStyles } from './ProfileSettings'; const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis }) => { - const { height: windowHeight } = useWindowDimensions(); - const { t } = useTranslation(); - const { colors } = useTheme(); + const { height: windowHeight } = useWindowDimensions(); + const { t } = useTranslation(); + const { colors } = useTheme(); - return ( - <> - setPrivacyVis(false)} transparent={true}> - setPrivacyVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - - - - - - - - - - - - ) -} + return ( + <> + setPrivacyVis(false)} transparent={true}> + setPrivacyVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + + + + + + + + + + + + ); +}; export default PrivacyPolicyModal; diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index e2e6d04fd..b081e642a 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -1,544 +1,696 @@ -import React, { useState, useEffect, useContext, useRef } from "react"; -import { Modal, StyleSheet, ScrollView } from "react-native"; -import { Dialog, Button, useTheme, Text, Appbar, IconButton, TextInput } from "react-native-paper"; -import { 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 PrivacyPolicyModal from "./PrivacyPolicyModal"; - -import {uploadFile} from "./uploadService"; -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 { resetDataAndRefresh } from "../config/dynamicConfig"; -import { AppContext } from "../App"; -import { shareQR } from "../components/QrCode"; -import { storageClear } from "../plugin/storage"; -import { getAppVersion } from "../plugin/clientStats"; -import { getConsentDocument } from "../splash/startprefs"; -import { logDebug } from "../plugin/logger"; +import React, { useState, useEffect, useContext, useRef } from 'react'; +import { Modal, StyleSheet, ScrollView } from 'react-native'; +import { Dialog, Button, useTheme, Text, Appbar, IconButton, TextInput } from 'react-native-paper'; +import { 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 PrivacyPolicyModal from './PrivacyPolicyModal'; + +import { uploadFile } from './uploadService'; +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 { resetDataAndRefresh } from '../config/dynamicConfig'; +import { AppContext } from '../App'; +import { shareQR } from '../components/QrCode'; +import { storageClear } from '../plugin/storage'; +import { getAppVersion } from '../plugin/clientStats'; +import { getConsentDocument } from '../splash/startprefs'; +import { logDebug } from '../plugin/logger'; //any pure functions can go outside const ProfileSettings = () => { - // anything that mutates must go in --- depend on props or state... - const { t } = useTranslation(); - const appConfig = useAppConfig(); - const { colors } = useTheme(); - const { setPermissionsPopupVis } = useContext(AppContext); - - //angular services needed - const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); - const EmailHelper = getAngularService('EmailHelper'); - const NotificationScheduler = getAngularService('NotificationScheduler'); - const ControlHelper = getAngularService('ControlHelper'); - - //functions that come directly from an Angular service - const editCollectionConfig = () => setEditCollectionVis(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 [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 [uploadVis, setUploadVis] = useState(false); - const [showingSensed, setShowingSensed] = useState(false); - const [showingLog, setShowingLog] = useState(false); - const [editSync, setEditSync] = useState(false); - const [editCollectionVis, setEditCollectionVis] = 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 [uiConfig, setUiConfig] = useState({}); - const [consentDoc, setConsentDoc] = useState({}); - const [dumpDate, setDumpDate] = useState(new Date()); - const [uploadReason, setUploadReason] = useState(""); - const appVersion = useRef(); - - 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(); - getAppVersion().then((version) => { - appVersion.current = version; - }); - } - - //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(); - } - - 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); + // anything that mutates must go in --- depend on props or state... + const { t } = useTranslation(); + const appConfig = useAppConfig(); + const { colors } = useTheme(); + const { setPermissionsPopupVis } = useContext(AppContext); + + //angular services needed + const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); + const EmailHelper = getAngularService('EmailHelper'); + const NotificationScheduler = getAngularService('NotificationScheduler'); + const ControlHelper = getAngularService('ControlHelper'); + + //functions that come directly from an Angular service + const editCollectionConfig = () => setEditCollectionVis(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 [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 [uploadVis, setUploadVis] = useState(false); + const [showingSensed, setShowingSensed] = useState(false); + const [showingLog, setShowingLog] = useState(false); + const [editSync, setEditSync] = useState(false); + const [editCollectionVis, setEditCollectionVis] = 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 [uiConfig, setUiConfig] = useState({}); + const [consentDoc, setConsentDoc] = useState({}); + const [dumpDate, setDumpDate] = useState(new Date()); + const [uploadReason, setUploadReason] = useState(''); + const appVersion = useRef(); + + 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); } - - //ensure ui table updated when editor closes - useEffect(() => { - if(editCollectionVis == false) { - setTimeout(function() { - console.log("closed editor, time to refresh collect"); - refreshCollectSettings(); - }, 1000); - } - }, [editCollectionVis]) - - 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); - } - - 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); - }); + }, [appConfig]); + + const refreshScreen = function () { + refreshCollectSettings(); + refreshNotificationSettings(); + getOPCode(); + getSyncSettings(); + getConnectURL(); + getAppVersion().then((version) => { + appVersion.current = version; + }); + }; + + //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`, }; - - //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); - }); + 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'; } - - 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 () { - if(uploadReason != "") { - let reason = uploadReason; - uploadFile("loggerDB", reason); - setUploadVis(false); - } + tempUiConfig.opcode = tempUiConfig.opcode || {}; + if (tempUiConfig.opcode.autogen == undefined) { + tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; } - 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(); - }); - } + // 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; } - 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)}, - }); - } + setCollectSettings(newCollectSettings); + } - async function userStartStopTracking() { - const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; - await forceTransition(transitionToForce); + //ensure ui table updated when editor closes + useEffect(() => { + if (editCollectionVis == false) { + setTimeout(function () { + console.log('closed editor, time to refresh collect'); refreshCollectSettings(); + }, 1000); } + }, [editCollectionVis]); - async function toggleLowAccuracy() { - let toggle = await helperToggleLowAccuracy(); - setTimeout(function() { - refreshCollectSettings(); - }, 1500); - } - - 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() { - getConsentDocument().then(function(resultDoc){ - setConsentDoc(resultDoc); - logDebug("In profile settings, consent doc found", resultDoc); - if (resultDoc == null) { - setNoConsentVis(true); - } else { - setConsentVis(true); - } - }, function(error) { - Logger.displayError("Error reading consent document from cache", error) - }); + 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); } - const onSelectState = function(stateObject) { - forceTransition(stateObject.transition); + 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; } - - 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(); + setAuthSettings(newAuthSettings); + } + + //methods that control the settings + const uploadLog = function () { + if (uploadReason != '') { + let reason = uploadReason; + uploadFile('loggerDB', reason); + setUploadVis(false); } - - //conditional creation of setting sections - - let logUploadSection; - console.debug("appConfg: support_upload:", appConfig?.profile_controls?.support_upload); - if (appConfig?.profile_controls?.support_upload) { - logUploadSection = setUploadVis(true)}>; + }; + + 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(); + }, + ); } - - let timePicker; - let notifSchedule; - if (appConfig?.reminderSchemes) - { - timePicker = ; - notifSchedule = <>console.log("")}> - + } + + 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'; + await forceTransition(transitionToForce); + refreshCollectSettings(); + } + + async function toggleLowAccuracy() { + let toggle = await helperToggleLowAccuracy(); + setTimeout(function () { + refreshCollectSettings(); + }, 1500); + } + + 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); + } } - - return ( - <> - - - {t('control.log-out')} - setLogoutVis(true)}> - - - - - - setPrivacyVis(true)}> - {timePicker} - - setPermissionsPopupVis(true)}> - - setCarbonDataVis(true)}> - setDateDumpVis(true)}> - {logUploadSection} - - - - - - - - {notifSchedule} - - setNukeVis(true)}> - setForceStateVis(true)}> - setShowingLog(true)}> - setShowingSensed(true)}> - - - - - - console.log("")} desc={appVersion.current}> - - - {/* 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 */} - {}}> - - {/* upload reason input */} - setUploadVis(false)} - transparent={true}> - setUploadVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('upload-service.upload-database', {db: "loggerDB"})} - - setUploadReason(uploadReason)} - placeholder={t('upload-service.please-fill-in-what-is-wrong')}> - - - - - - - - - - {/* opcode viewing popup */} - shareQR(authSettings.opcode)}> - - {/* {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')} - - {t('general-settings.no-consent-logout')} - - - - - - - - {/* handle consent */} - setConsentVis(false)} transparent={true}> - setConsentVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.consented-to', {approval_date: consentDoc.approval_date})} - - - - - - - - - - - - - - - - - - - + }; + + 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() { + getConsentDocument().then( + function (resultDoc) { + setConsentDoc(resultDoc); + logDebug('In profile settings, consent doc found', 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 = ( + setUploadVis(true)}> + ); + } + + let timePicker; + let notifSchedule; + if (appConfig?.reminderSchemes) { + timePicker = ( + + ); + notifSchedule = ( + <> + console.log('')}> + + + ); + } + + return ( + <> + + + {t('control.log-out')} + setLogoutVis(true)}> + + + + + + setPrivacyVis(true)}> + {timePicker} + + setPermissionsPopupVis(true)}> + + setCarbonDataVis(true)}> + setDateDumpVis(true)}> + {logUploadSection} + + + + + + + + {notifSchedule} + + setNukeVis(true)}> + setForceStateVis(true)}> + setShowingLog(true)}> + setShowingSensed(true)}> + + + + + + console.log('')} + desc={appVersion.current}> + + + {/* 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 */} + {}}> + + {/* upload reason input */} + setUploadVis(false)} transparent={true}> + setUploadVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('upload-service.upload-database', { db: 'loggerDB' })} + + setUploadReason(uploadReason)} + placeholder={t('upload-service.please-fill-in-what-is-wrong')}> + + + + + + + + + {/* opcode viewing popup */} + shareQR(authSettings.opcode)}> + + {/* {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')} + + {t('general-settings.no-consent-logout')} + + + + + + + + {/* handle consent */} + setConsentVis(false)} transparent={true}> + setConsentVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + + {t('general-settings.consented-to', { 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", - } - }); - - export default ProfileSettings; + dialog: (surfaceColor) => ({ + backgroundColor: surfaceColor, + margin: 5, + marginLeft: 25, + marginRight: 25, + }), + monoDesc: { + fontSize: 12, + fontFamily: 'monospace', + }, +}); + +export default ProfileSettings; diff --git a/www/js/control/ReminderTime.tsx b/www/js/control/ReminderTime.tsx index 40e8485ee..b603758b0 100644 --- a/www/js/control/ReminderTime.tsx +++ b/www/js/control/ReminderTime.tsx @@ -1,69 +1,70 @@ -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 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 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}> - - - ) -} + 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; \ No newline at end of file +export default ReminderTime; diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index b746dfc8d..82fa60581 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -1,91 +1,101 @@ -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"); - } + 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); - } + 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); } + } + + useEffect(() => { + updateEntries(); + }, [pageVis]); - useEffect(() => { - updateEntries(); - }, [pageVis]); + const separator = () => ; + const cacheItem = ({ item: cacheItem }) => ( + + + {cacheItem.metadata.write_fmt_time} + + + {cacheItem.data} + + + ); - const separator = () => - const cacheItem = ({item: cacheItem}) => ( - {cacheItem.metadata.write_fmt_time} - {cacheItem.data} - ); + return ( + setPageVis(false)}> + + + setPageVis(false)} /> + + - return ( - setPageVis(false)}> - - - setPageVis(false)}/> - - + + updateEntries()} /> + emailCache()} /> + - - updateEntries()}/> - emailCache()}/> - - - item.metadata.write_ts} - ItemSeparatorComponent={separator} - /> - - - ); + 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; diff --git a/www/js/control/SettingRow.jsx b/www/js/control/SettingRow.jsx index 473a45d7f..b55b3c804 100644 --- a/www/js/control/SettingRow.jsx +++ b/www/js/control/SettingRow.jsx @@ -1,52 +1,59 @@ -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 0374adf5a..8eeaf39bb 100644 --- a/www/js/control/emailService.js +++ b/www/js/control/emailService.js @@ -2,96 +2,113 @@ 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) { + .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); + }); + }); + }); + }; - 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); - }); - }); - }); - } - - const hasAccount = function() { - return new Promise(function(resolve, reject) { - $window.cordova.plugins.email.hasAccount(function (hasAct) { - resolve(hasAct); - }); - }); - } + const hasAccount = function () { + return new Promise(function (resolve, reject) { + $window.cordova.plugins.email.hasAccount(function (hasAct) { + resolve(hasAct); + }); + }); + }; - this.sendEmail = function (database) { - Promise.all([getEmailConfig(), hasAccount()]).then(function([address, hasAct]) { - var parentDir = "unknown"; + this.sendEmail = function (database) { + Promise.all([getEmailConfig(), hasAccount()]).then(function ([address, hasAct]) { + var parentDir = 'unknown'; - // 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 + // 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.isIOS() && !hasAct) { - alert(i18next.t('email-service.email-account-not-configured')); - return; - } + if (ionic.Platform.isIOS() && !hasAct) { + alert(i18next.t('email-service.email-account-not-configured')); + return; + } - 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.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 + "!") - } + 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') - } - - $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; - }); - }); + 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; + }); + }); + }; + }); diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index 038bd8efc..2b7520edb 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -1,129 +1,135 @@ -import { logDebug, logInfo, displayError } from "../plugin/logger"; -import i18next from "i18next"; +import { logDebug, logInfo, displayError } from '../plugin/logger'; +import i18next from 'i18next'; /** * @returns A promise that resolves with an upload URL or rejects with an error */ async function getUploadConfig() { - return new Promise(async function (resolve, reject) { - logInfo( "About to get email config"); - let url = []; - try { - let response = await fetch("json/uploadConfig.json"); - let uploadConfig = await response.json(); - logDebug("uploadConfigString = " + JSON.stringify(uploadConfig['url'])); - url.push(uploadConfig["url"]); - resolve(url); - } catch (err) { - try{ - let response = await fetch("json/uploadConfig.json.sample"); - let uploadConfig = await response.json(); - logDebug("default uploadConfigString = " + JSON.stringify(uploadConfig['url'])); - console.log("default uploadConfigString = " + JSON.stringify(uploadConfig['url'])) - url.push(uploadConfig["url"]); - resolve(url); - } catch (err) { - displayError(err, "Error while reading default upload config"); - reject(err); - } - } - }) + return new Promise(async function (resolve, reject) { + logInfo('About to get email config'); + let url = []; + try { + let response = await fetch('json/uploadConfig.json'); + let uploadConfig = await response.json(); + logDebug('uploadConfigString = ' + JSON.stringify(uploadConfig['url'])); + url.push(uploadConfig['url']); + resolve(url); + } catch (err) { + try { + let response = await fetch('json/uploadConfig.json.sample'); + let uploadConfig = await response.json(); + logDebug('default uploadConfigString = ' + JSON.stringify(uploadConfig['url'])); + console.log('default uploadConfigString = ' + JSON.stringify(uploadConfig['url'])); + url.push(uploadConfig['url']); + resolve(url); + } catch (err) { + displayError(err, 'Error while reading default upload config'); + reject(err); + } + } + }); } function onReadError(err) { - displayError(err, "Error while reading log"); + displayError(err, 'Error while reading log'); } function onUploadError(err) { - displayError(err, "Error while uploading log"); + displayError(err, 'Error while uploading log'); } function readDBFile(parentDir, database, callbackFn) { - return new Promise(function(resolve, reject) { - window['resolveLocalFileSystemURL'](parentDir, function(fs) { - console.log("resolving file system as ", fs); - fs.filesystem.root.getFile(fs.fullPath+database, null, (fileEntry) => { - console.log(fileEntry); - fileEntry.file(function(file) { - console.log(file); - var reader = new FileReader(); + return new Promise(function (resolve, reject) { + window['resolveLocalFileSystemURL'](parentDir, function (fs) { + console.log('resolving file system as ', 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.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.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 as ArrayBuffer)); - } + reader.onload = function () { + console.log('Successful file read with ' + this.result['byteLength'] + ' characters'); + resolve(new DataView(this.result as ArrayBuffer)); + }; - reader.readAsArrayBuffer(file); - }, reject); - }, reject); - }); + reader.readAsArrayBuffer(file); + }, reject); + }, + reject, + ); }); + }); } const sendToServer = function upload(url, binArray, params) { - //use url encoding to pass additional params in the post - const urlParams = "?reason=" + params.reason + "&tz=" + params.tz; - return fetch(url+urlParams, { - method: 'POST', - headers: {'Content-Type': undefined }, - body: binArray - } ) -} + //use url encoding to pass additional params in the post + const urlParams = '?reason=' + params.reason + '&tz=' + params.tz; + return fetch(url + urlParams, { + method: 'POST', + headers: { 'Content-Type': undefined }, + body: binArray, + }); +}; //only export of this file, used in ProfileSettings and passed the argument (""loggerDB"") export async function uploadFile(database, reason) { - try { - let uploadConfig = await getUploadConfig(); - var parentDir = "unknown"; + try { + let uploadConfig = await getUploadConfig(); + var parentDir = 'unknown'; - if (window['cordova'].platformId.toLowerCase() == "android") { - parentDir = window['cordova'].file.applicationStorageDirectory+"/databases"; - } - else if (window['cordova'].platformId.toLowerCase() == "ios") { - parentDir = window['cordova'].file.dataDirectory + "../LocalDatabase"; - } else { - alert("parentDir unexpectedly = " + parentDir + "!") - } + if (window['cordova'].platformId.toLowerCase() == 'android') { + parentDir = window['cordova'].file.applicationStorageDirectory + '/databases'; + } else if (window['cordova'].platformId.toLowerCase() == 'ios') { + parentDir = window['cordova'].file.dataDirectory + '../LocalDatabase'; + } else { + alert('parentDir unexpectedly = ' + parentDir + '!'); + } - logInfo("Going to upload " + database); - try { - let binString = await readDBFile(parentDir, database, undefined); - console.log("Uploading file of size "+binString['byteLength']); - const params = { - reason: reason, - tz: Intl.DateTimeFormat().resolvedOptions().timeZone - } - uploadConfig.forEach(async (url) => { - //have alert for starting upload, but not progress - window.alert(i18next.t("upload-service.upload-database", {db: database})); + logInfo('Going to upload ' + database); + try { + let binString = await readDBFile(parentDir, database, undefined); + console.log('Uploading file of size ' + binString['byteLength']); + const params = { + reason: reason, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + uploadConfig.forEach(async (url) => { + //have alert for starting upload, but not progress + window.alert(i18next.t('upload-service.upload-database', { db: database })); - try { - let response = await sendToServer(url, binString, params); - window.alert(i18next.t("upload-service.upload-details", - {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: url}) - + i18next.t("upload-service.upload-success")); - return response; - } catch (error) { - onUploadError(error); - } - }); - } - catch (error){ - onReadError(error); + try { + let response = await sendToServer(url, binString, params); + window.alert( + i18next.t('upload-service.upload-details', { + filesizemb: binString['byteLength'] / (1000 * 1000), + serverURL: url, + }) + i18next.t('upload-service.upload-success'), + ); + return response; + } catch (error) { + onUploadError(error); } + }); } catch (error) { - onReadError(error); + onReadError(error); } -}; \ No newline at end of file + } catch (error) { + onReadError(error); + } +} diff --git a/www/js/controllers.js b/www/js/controllers.js index 5a4de0cb4..abf5916c5 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -4,85 +4,112 @@ import angular from 'angular'; import { addStatError, addStatReading, statKeys } from './plugin/clientStats'; import { getPendingOnboardingState } from './onboarding/onboardingHelper'; -angular.module('emission.controllers', ['emission.splash.pushnotify', - 'emission.splash.storedevicesettings', - 'emission.splash.localnotify', - 'emission.splash.remotenotify']) +angular + .module('emission.controllers', [ + 'emission.splash.pushnotify', + 'emission.splash.storedevicesettings', + 'emission.splash.localnotify', + 'emission.splash.remotenotify', + ]) -.controller('RootCtrl', function($scope) {}) + .controller('RootCtrl', function ($scope) {}) -.controller('DashCtrl', function($scope) {}) + .controller('DashCtrl', function ($scope) {}) -.controller('SplashCtrl', function($scope, $state, $interval, $rootScope, - PushNotify, StoreDeviceSettings, - LocalNotify, RemoteNotify) { - console.log('SplashCtrl invoked'); - // alert("attach debugger!"); - // PushNotify.startupInit(); + .controller( + 'SplashCtrl', + function ( + $scope, + $state, + $interval, + $rootScope, + PushNotify, + StoreDeviceSettings, + LocalNotify, + RemoteNotify, + ) { + 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)); - addStatReading(statKeys.STATE_CHANGED, fromState.name + '-2-' + toState.name); - }); - $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)); - addStatError(statKeys.STATE_CHANGED, fromState.name + '-2-' + toState.name+ "_" + error); - }); - $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 - addStatError(statKeys.STATE_CHANGED, fromState.name + '-2-' + unfoundState.name); - }); - - var isInList = function(element, list) { - return list.indexOf(element) != -1 - } + $rootScope.$on( + '$stateChangeSuccess', + function (event, toState, toParams, fromState, fromParams) { + console.log( + 'Finished changing state from ' + + JSON.stringify(fromState) + + ' to ' + + JSON.stringify(toState), + ); + addStatReading(statKeys.STATE_CHANGED, fromState.name + '-2-' + toState.name); + }, + ); + $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), + ); + addStatError(statKeys.STATE_CHANGED, fromState.name + '-2-' + toState.name + '_' + error); + }, + ); + $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 + addStatError(statKeys.STATE_CHANGED, fromState.name + '-2-' + unfoundState.name); + }); - $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 - 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'); -}) + 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 + 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 c0b7bce35..7c8294005 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -1,20 +1,22 @@ import angular from 'angular'; import LabelTab from './diary/LabelTab'; -angular.module('emission.main.diary',['emission.main.diary.services', - 'emission.survey.multilabel.buttons', - 'emission.survey.enketo.add-note-button', - 'emission.survey.enketo.trip.button', - 'emission.plugin.logger']) +angular + .module('emission.main.diary', [ + 'emission.main.diary.services', + 'emission.survey.multilabel.buttons', + 'emission.survey.enketo.add-note-button', + 'emission.survey.enketo.trip.button', + 'emission.plugin.logger', + ]) -.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 f4677766d..8b6e65d52 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -6,23 +6,28 @@ share the data that has been loaded and interacted with. */ -import React, { useEffect, useState, useRef } from "react"; -import { 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 { useTheme } from "react-native-paper"; -import { getPipelineRangeTs } from "../commHelper"; +import React, { useEffect, useState, useRef } from 'react'; +import { 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 { useTheme } from 'react-native-paper'; +import { getPipelineRangeTs } from '../commHelper'; let labelPopulateFactory, labelsResultMap, notesResultMap, showPlaces; const ONE_DAY = 24 * 60 * 60; // seconds @@ -42,7 +47,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'); @@ -70,7 +75,8 @@ const LabelTab = () => { // initalize filters const tripFilter = surveyOpt.filter; const allFalseFilters = tripFilter.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); } @@ -86,7 +92,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 */ @@ -106,12 +112,20 @@ const LabelTab = () => { async function loadTimelineEntries() { try { const pipelineRange = await 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); } } @@ -131,34 +145,39 @@ 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 })); } } @@ -170,20 +189,30 @@ 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])); @@ -192,13 +221,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; } @@ -206,16 +235,22 @@ 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') + const lastProcessedTrip = + timelineMap && + [...timelineMap?.values()] + .reverse() + .find((trip) => trip.origin_key.includes('confirmed_trip')); + readUnprocessedPromise = Timeline.readUnprocessedTrips( + pipelineRange.end_ts, + nowTs, + lastProcessedTrip, ); - 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; @@ -225,10 +260,15 @@ 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); @@ -239,10 +279,13 @@ 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); } @@ -261,24 +304,27 @@ const LabelTab = () => { loadSpecificWeek, refresh, repopulateTimelineEntry, - } + }; const Tab = createStackNavigator(); return ( - + - + options={{ detachPreviousScreen: false }} + /> ); -} +}; export default LabelTab; diff --git a/www/js/diary/addressNamesHelper.ts b/www/js/diary/addressNamesHelper.ts index e7f198fbe..f0e17921a 100644 --- a/www/js/diary/addressNamesHelper.ts +++ b/www/js/diary/addressNamesHelper.ts @@ -29,9 +29,7 @@ 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)); }, }; } @@ -41,7 +39,6 @@ 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); @@ -63,8 +60,7 @@ 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') { @@ -77,11 +73,8 @@ 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 = () => { @@ -93,15 +86,19 @@ 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 '...'; } @@ -115,31 +112,42 @@ 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 f97a38e46..f6e845983 100644 --- a/www/js/diary/cards/DiaryCard.tsx +++ b/www/js/diary/cards/DiaryCard.tsx @@ -7,35 +7,53 @@ (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 5211f7ed4..37788a789 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,14 +8,13 @@ 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(.8).rgb().string(); - let indicatorBorderColor = color('black').alpha(.5).rgb().string(); + const indicatorBackgroundColor = color(colors.onPrimary).alpha(0.8).rgb().string(); + let indicatorBorderColor = color('black').alpha(0.5).rgb().string(); let modeViews; if (trip.userInput.MODE) { @@ -25,35 +24,56 @@ 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 cd1d9c10e..a351f696f 100644 --- a/www/js/diary/cards/PlaceCard.tsx +++ b/www/js/diary/cards/PlaceCard.tsx @@ -6,45 +6,52 @@ 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 = 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 0e8903ec5..10a97e6ee 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 { View, StyleSheet } from "react-native"; -import { Text, useTheme } from "react-native-paper"; +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; type Props = { - lightBg: boolean, - time: string, - date?: string, + lightBg: boolean; + time: string; + date?: string; }; const TimestampBadge = ({ lightBg, time, date, ...otherProps }: Props) => { const { colors } = useTheme(); @@ -16,14 +16,18 @@ const TimestampBadge = ({ lightBg, time, date, ...otherProps }: Props) => { const textColor = lightBg ? 'black' : 'white'; return ( - - - {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 08e02bca4..78ef42fe1 100644 --- a/www/js/diary/cards/TripCard.tsx +++ b/www/js/diary/cards/TripCard.tsx @@ -4,35 +4,41 @@ 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 = 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); @@ -42,7 +48,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 }; @@ -50,52 +56,82 @@ 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 855c50ed4..07b5caf71 100644 --- a/www/js/diary/cards/UntrackedTimeCard.tsx +++ b/www/js/diary/cards/UntrackedTimeCard.tsx @@ -7,42 +7,57 @@ 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} + /> @@ -54,7 +69,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 8d1096fab..b25facc57 100644 --- a/www/js/diary/components/StartEndLocations.tsx +++ b/www/js/diary/components/StartEndLocations.tsx @@ -4,67 +4,70 @@ 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 (<> - - {props.displayStartTime && - - {props.displayStartTime} - - } - - - - - {props.displayStartName} - - - {(props.displayEndName != undefined) && <> - + return ( + <> - {props.displayEndTime && - - {props.displayEndTime} - - } - - + {props.displayStartTime && ( + {props.displayStartTime} + )} + + - - {props.displayEndName} + + {props.displayStartName} - } - ); -} + {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 ffed9a300..ed48f89c9 100644 --- a/www/js/diary/details/LabelDetailsScreen.tsx +++ b/www/js/diary/details/LabelDetailsScreen.tsx @@ -2,25 +2,32 @@ 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(); @@ -28,58 +35,91 @@ 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 */} @@ -87,13 +127,9 @@ 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 3902c8afe..8030842df 100644 --- a/www/js/diary/details/OverallTripDescriptives.tsx +++ b/www/js/diary/details/OverallTripDescriptives.tsx @@ -1,42 +1,45 @@ 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 6d172fed4..5bd30fdd5 100644 --- a/www/js/diary/details/TripSectionsDescriptives.tsx +++ b/www/js/diary/details/TripSectionsDescriptives.tsx @@ -1,65 +1,82 @@ 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 0b834a485..48f40322d 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -1,57 +1,67 @@ // 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; @@ -59,27 +69,29 @@ 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'); } /** @@ -90,7 +102,10 @@ 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') + ); } /** @@ -138,11 +153,10 @@ 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 []; @@ -157,14 +171,16 @@ 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% }; }); @@ -178,7 +194,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', })); } @@ -186,6 +202,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 bcaeb83ae..5755c91ab 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 1c28cdc2c..515553851 100644 --- a/www/js/diary/list/DateSelect.tsx +++ b/www/js/diary/list/DateSelect.tsx @@ -6,18 +6,17 @@ 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(); @@ -57,36 +56,48 @@ 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 d1906f462..0018c1bc5 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,28 +44,32 @@ 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 4fb1702b2..217115938 100644 --- a/www/js/diary/list/LabelListScreen.tsx +++ b/www/js/diary/list/LabelListScreen.tsx @@ -1,38 +1,61 @@ -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 f3d6db082..dfc49a9e2 100644 --- a/www/js/diary/list/LoadMoreButton.tsx +++ b/www/js/diary/list/LoadMoreButton.tsx @@ -1,18 +1,24 @@ -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: { @@ -21,8 +27,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 6dfd1e736..954a90db9 100644 --- a/www/js/diary/list/TimelineScrollList.tsx +++ b/www/js/diary/list/TimelineScrollList.tsx @@ -11,51 +11,58 @@ 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')} ); @@ -64,7 +71,7 @@ const TimelineScrollList = ({ listEntries, queriedRange, pipelineRange, loadMore /* 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) { @@ -73,7 +80,8 @@ const TimelineScrollList = ({ listEntries, queriedRange, pipelineRange, loadMore } 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 774273fa2..92d322f04 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -6,47 +6,56 @@ import { SurveyOptions } from '../survey/survey'; import { getConfig } from '../config/dynamicConfig'; import { getRawEntries } from '../commHelper'; -angular.module('emission.main.diary.services', ['emission.plugin.logger', - 'emission.services']) -.factory('Timeline', function($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 () { - getConfig().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 ( + $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 () { + getConfig().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, - }); - - timeline.readAllCompositeTrips = function(startTs, endTs) { - $ionicLoading.show({ - template: i18next.t('service.reading-server') + + // 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, }); - const readPromises = [ - getRawEntries(["analysis/composite_trip"], - startTs, endTs, "data.end_ts"), - ]; - return Promise.all(readPromises) - .then(([ctList]) => { + + timeline.readAllCompositeTrips = function (startTs, endTs) { + $ionicLoading.show({ + template: i18next.t('service.reading-server'), + }); + const readPromises = [ + 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); @@ -56,191 +65,222 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', 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); @@ -250,17 +290,26 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', } 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); @@ -268,121 +317,130 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', 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) + 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, ); - 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 579e3ac4f..be6ee1bb3 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,7 +70,14 @@ 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) { @@ -97,9 +104,9 @@ export function populateCompositeTrips(ctList, showPlaces, labelsFactory, labels } 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) { @@ -128,10 +135,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); } @@ -150,10 +157,12 @@ 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); } @@ -164,14 +173,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, - } + }, }); /** @@ -188,25 +197,23 @@ 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 604fef227..fe324ee3f 100644 --- a/www/js/diary/useDerivedProperties.tsx +++ b/www/js/diary/useDerivedProperties.tsx @@ -1,9 +1,16 @@ -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(() => { @@ -12,7 +19,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), @@ -24,8 +31,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 45cca7043..bcfb74391 100644 --- a/www/js/i18n-utils.js +++ b/www/js/i18n-utils.js @@ -2,39 +2,48 @@ 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 a2688d66e..c2093c698 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' && typeof lang[key] === 'object') { lang[key] = {}; mergeInTranslations(lang[key], value); @@ -30,11 +30,11 @@ const mergeInTranslations = (lang, fallbackLang) => { lang[key] = value; } } else if (typeof value === 'object' && typeof lang[key] === '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,22 +59,24 @@ 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/main.js b/www/js/main.js index 91437a07a..2b351e2c4 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -2,29 +2,40 @@ import angular from 'angular'; -angular.module('emission.main', ['emission.main.diary', - 'emission.i18n.utils', - 'emission.splash.notifscheduler', - 'emission.main.metrics.factory', - 'emission.main.metrics.mappings', - 'emission.services']) +angular + .module('emission.main', [ + 'emission.main.diary', + 'emission.i18n.utils', + 'emission.splash.notifscheduler', + 'emission.main.metrics.factory', + 'emission.main.metrics.mappings', + 'emission.services', + ]) -.config(function($stateProvider) { - $stateProvider.state('root.main', { - url: '/main', - template: `` - }); -}) + .config(function ($stateProvider) { + $stateProvider.state('root.main', { + url: '/main', + 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); - }); - } -}); + .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, + ); + }, + ); + }; + }); diff --git a/www/js/metrics-factory.js b/www/js/metrics-factory.js index b5ead9c82..28ef5ae9e 100644 --- a/www/js/metrics-factory.js +++ b/www/js/metrics-factory.js @@ -1,238 +1,271 @@ 'use strict'; import angular from 'angular'; -import { getBaseModeByValue } from './diary/diaryHelper' +import { getBaseModeByValue } from './diary/diaryHelper'; import { labelOptions } from './survey/multilabel/confirmHelper'; import { storageGet, storageRemove, storageSet } from './plugin/storage'; -angular.module('emission.main.metrics.factory', - ['emission.main.metrics.mappings']) +angular + .module('emission.main.metrics.factory', ['emission.main.metrics.mappings']) -.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'; } + }; - 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 + 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); + } } - 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"); } - else { - if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { - console.log("Non motorized mode or footprint <= range_limited_motorized", mode, footprint[mode], rlmCO2); + 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 { 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; -}) + } + }; -.factory('CalorieCal', function(METDatasetHelper, CustomDatasetHelper) { + return fh; + }) - var cc = {}; - var highestMET = 0; - var USER_DATA_KEY = "user-data"; - cc.useCustom = false; + .factory('CalorieCal', function (METDatasetHelper, CustomDatasetHelper) { + var cc = {}; + var highestMET = 0; + var USER_DATA_KEY = 'user-data'; + cc.useCustom = false; - cc.setUseCustomFootprint = function () { - cc.useCustom = true; - } + cc.setUseCustomFootprint = function () { + cc.useCustom = true; + }; - cc.getMETs = function() { - if (this.useCustom == true) { + cc.getMETs = function () { + if (this.useCustom == true) { return CustomDatasetHelper.getCustomMETs(); - } else { + } else { return METDatasetHelper.getStandardMETs(); - } - } - - cc.set = function(info) { - return storageSet(USER_DATA_KEY, info); - }; - cc.get = function() { - return storageGet(USER_DATA_KEY); - }; - cc.delete = function() { - return storageRemove(USER_DATA_KEY); - }; - Number.prototype.between = function (min, max) { - return this >= min && this <= max; - }; - cc.getHighestMET = function() { - if (!highestMET) { + } + }; + + cc.set = function (info) { + return storageSet(USER_DATA_KEY, info); + }; + cc.get = function () { + return storageGet(USER_DATA_KEY); + }; + cc.delete = function () { + return storageRemove(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; } - } - } - 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; -}); + 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; + }); diff --git a/www/js/metrics-mappings.js b/www/js/metrics-mappings.js index 60068711d..38836a3a1 100644 --- a/www/js/metrics-mappings.js +++ b/www/js/metrics-mappings.js @@ -3,399 +3,423 @@ import { getLabelOptions } from './survey/multilabel/confirmHelper'; import { getConfig } from './config/dynamicConfig'; import { storageGet, storageSet } from './plugin/storage'; -angular.module('emission.main.metrics.mappings', ['emission.plugin.logger']) +angular + .module('emission.main.metrics.mappings', ['emission.plugin.logger']) -.service('CarbonDatasetHelper', function() { - var CARBON_DATASET_KEY = 'carbon_dataset_locale'; + .service('CarbonDatasetHelper', function () { + 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 storageGet(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.loadCarbonDatasetLocale = function () { + return storageGet(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) { - setCurrentCarbonDatasetLocale(localeCode); - storageSet(CARBON_DATASET_KEY, currentCarbonDatasetCode); - Logger.log("CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [" + currentCarbonDatasetCode + "] to storage"); - } + this.saveCurrentCarbonDatasetLocale = function (localeCode) { + setCurrentCarbonDatasetLocale(localeCode); + storageSet(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.getCarbonDatasetOptions = function () { + var options = []; + for (var code in carbonDatasets) { + options.push({ + text: code, //carbonDatasets[code].regionName, + value: code, + }); + } + return options; + }; - this.getCurrentCarbonDatasetCode = function () { - return currentCarbonDatasetCode; - }; + this.getCurrentCarbonDatasetCode = function () { + return currentCarbonDatasetCode; + }; - this.getCurrentCarbonDatasetFootprint = function () { - return carbonDatasets[currentCarbonDatasetCode].footprintData; - }; -}) -.service('METDatasetHelper', function() { - 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 + this.getCurrentCarbonDatasetFootprint = function () { + return carbonDatasets[currentCarbonDatasetCode].footprintData; + }; + }) + .service('METDatasetHelper', function () { + 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, + }, }, - "VERY_FAST_0": { - range: [3.5, 4.0], - mets: 5.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, + }, }, - "VERY_FAST_!": { - range: [4.0, 4.5], - mets: 6.0 + UNKNOWN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "VERY_VERY_FAST": { - range: [4.5, 5], - mets: 7.0 + IN_VEHICLE: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "SUPER_FAST": { - range: [5, 6], - mets: 8.3 + CAR: { + 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 + BUS: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "VERY_SLOW": { - range: [5.5, 10], - mets: 5.8 + LIGHT_RAIL: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "SLOW": { - range: [10, 12], - mets: 6.8 + TRAIN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "MODERATE": { - range: [12, 14], - mets: 8.0 + TRAM: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "FAST": { - range: [14, 16], - mets: 10.0 + SUBWAY: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "VERT_FAST": { - range: [16, 19], - mets: 12.0 + AIR_OR_HSR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 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) { + }; + this.getStandardMETs = function () { + return standardMETs; + }; + }) + .factory('CustomDatasetHelper', function (METDatasetHelper, Logger, $ionicPlatform) { var cdh = {}; - cdh.getCustomMETs = function() { - console.log("Getting custom METs", cdh.customMETs); - return 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); - return cdh.customPerKmFootprint; + cdh.getCustomFootprint = function () { + console.log('Getting custom footprint', cdh.customPerKmFootprint); + return cdh.customPerKmFootprint; }; - cdh.populateCustomMETs = function() { - let standardMETs = METDatasetHelper.getStandardMETs(); - 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]; - } else { - console.warn("Did not find either met_equivalent or met for " - +opt.value+" ignoring entry"); - return undefined; - } + cdh.populateCustomMETs = function () { + let standardMETs = METDatasetHelper.getStandardMETs(); + 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, + ); } - }); - cdh.customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); - console.log("After populating, custom METs = ", cdh.customMETs); + 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); }; - 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); + 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, + }); } - if (angular.isDefined(opt.kgCo2PerKm)) { - return [opt.value, opt.kgCo2PerKm]; - } else { - return undefined; - } - }).filter((modeCO2) => angular.isDefined(modeCO2));; - cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); - console.log("After populating, custom perKmFootprint", cdh.customPerKmFootprint); - } + 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]; + } else { + return undefined; + } + }) + .filter((modeCO2) => angular.isDefined(modeCO2)); + cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); + console.log('After populating, custom perKmFootprint', cdh.customPerKmFootprint); + }; - cdh.init = function(newConfig) { + cdh.init = function (newConfig) { try { getLabelOptions(newConfig).then((inputParams) => { - console.log("Input params = ", 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); + Logger.displayError( + 'Error in metrics-mappings while initializing custom dataset helper', + e, + ); }, 1000); } - } + }; - $ionicPlatform.ready().then(function() { + $ionicPlatform.ready().then(function () { getConfig().then((newConfig) => cdh.init(newConfig)); }); return cdh; -}); + }); diff --git a/www/js/metrics/ActiveMinutesTableCard.tsx b/www/js/metrics/ActiveMinutesTableCard.tsx index ea360ce8e..2ed26ccfc 100644 --- a/www/js/metrics/ActiveMinutesTableCard.tsx +++ b/www/js/metrics/ActiveMinutesTableCard.tsx @@ -2,24 +2,26 @@ 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); @@ -28,30 +30,32 @@ 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); + 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; }) - totals['period'] = formatDate(day); - return totals; - }).reverse(); + .reverse(); }, [userMetrics?.duration]); const allTotals = [cumulativeTotals, ...recentWeeksActiveModesTotals, ...dailyActiveModesTotals]; @@ -62,38 +66,46 @@ 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 6012cb61a..7c9bf3891 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -1,168 +1,240 @@ 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; + 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; } - }, [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')} - - } - - - ) -} + } + + 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')} + + + )} + + + ); +}; export default CarbonFootprintCard; diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 223ae709f..9f1b4490f 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -1,151 +1,189 @@ 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 = []; - - //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)}`}); - } + 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'); + } - //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()), + //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.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; - if (userPastWeek.low == userPastWeek.high) - textList.push({label: label, value: Math.round(userPastWeek.low)}); + 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(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)}); + 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)}`, + }); - return textList; + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + textList.push({ label: t('main-metrics.worst-case'), value: Math.round(worstCarbon) }); + + 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 groupText = []; + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'aggregate'); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, 'distance'); - let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), + // 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; } - 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 groupText = []; - return 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)}`, + }); + + 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 eafd3460e..a2373faf3 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -1,79 +1,72 @@ -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 479a5f5b5..acaf9c1ed 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -1,7 +1,6 @@ - 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'; @@ -10,19 +9,18 @@ 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), @@ -31,34 +29,38 @@ 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 7a0f8c8bc..1727d6e49 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,8 +1,7 @@ - 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'; @@ -13,30 +12,36 @@ 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), @@ -47,7 +52,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }); // 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 }); @@ -55,8 +60,8 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }, [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]); @@ -67,10 +72,8 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni // 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; @@ -79,55 +82,84 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni // 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 c66218453..fa1aaed3e 100644 --- a/www/js/metrics/MetricsDateSelect.tsx +++ b/www/js/metrics/MetricsDateSelect.tsx @@ -6,66 +6,78 @@ 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 450155622..d23cdd454 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -1,36 +1,35 @@ -import React, { useEffect, useState, useMemo } from "react"; -import { 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 { getAggregateData, getMetrics } from "../commHelper"; +import React, { useEffect, useState, useMemo } from 'react'; +import { 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 { getAggregateData, getMetrics } from '../commHelper'; 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 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 getMetrics('timestamp', query); - return getAggregateData("result/metrics/timestamp", query); + is_return_aggregate: type == 'aggregate', + }; + if (type == 'user') return getMetrics('timestamp', query); + return getAggregateData('result/metrics/timestamp', query); } function getLastTwoWeeksDtRange() { @@ -41,10 +40,9 @@ 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); @@ -55,11 +53,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]; }); @@ -75,49 +73,60 @@ const MetricsTab = () => { } const { width: windowWidth } = useWindowDimensions(); - const cardWidth = windowWidth * .88; + const cardWidth = windowWidth * 0.88; - return (<> - - - - - - - - - - - - - - - - - - - - {/* + + + + + + + + + + + + + + + + + + + + {/* */} - - - ); -} + + + + ); +}; export const cardMargin = 10; @@ -134,7 +143,7 @@ export const cardStyles: any = { titleText: (colors) => ({ color: colors.onPrimary, fontWeight: '500', - textAlign: 'center' + textAlign: 'center', }), subtitleText: { fontSize: 13, @@ -146,7 +155,7 @@ export const cardStyles: any = { padding: 8, paddingBottom: 12, flex: 1, - } -} + }, +}; export default MetricsTab; diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index 99bf9d425..387ebc79d 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -1,7 +1,6 @@ - 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'; @@ -11,68 +10,70 @@ 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 d1cd435d4..3df71cdc1 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,42 +16,39 @@ 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}`; } @@ -61,50 +58,49 @@ 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); @@ -114,49 +110,56 @@ 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 { @@ -170,13 +173,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 = []; @@ -185,28 +188,34 @@ 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; -} \ No newline at end of file +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; +}; diff --git a/www/js/metrics/metricsTypes.ts b/www/js/metrics/metricsTypes.ts index d51c98b3a..cfe4444a3 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/ngApp.js b/www/js/ngApp.js index f82c53482..228c2a989 100644 --- a/www/js/ngApp.js +++ b/www/js/ngApp.js @@ -32,51 +32,61 @@ import App from './App'; import { getTheme } from './appTheme'; import { SafeAreaView } from 'react-native-safe-area-context'; -angular.module('emission', ['ionic', 'jm.i18next', - 'emission.controllers','emission.services', 'emission.plugin.logger', - 'emission.splash.referral', 'emission.services.email', - 'emission.main', 'pascalprecht.translate', 'LocalStorageModule']) +angular + .module('emission', [ + 'ionic', + 'jm.i18next', + 'emission.controllers', + 'emission.services', + 'emission.plugin.logger', + 'emission.splash.referral', + 'emission.services.email', + 'emission.main', + 'pascalprecht.translate', + 'LocalStorageModule', + ]) -.run(function($ionicPlatform, $rootScope, $http, Logger, localStorageService) { - 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"); - $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"); + .run(function ($ionicPlatform, $rootScope, $http, Logger, localStorageService) { + 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"); + $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"); + 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'); - const rootEl = document.getElementById('appRoot'); - const reactRoot = createRoot(rootEl); + const rootEl = document.getElementById('appRoot'); + const reactRoot = createRoot(rootEl); - const theme = getTheme(); + const theme = getTheme(); - reactRoot.render( - - - - - - - ); + + + + + , + ); + }); + console.log('Ending run'); }); - console.log("Ending run"); -}); diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx index c547fd074..cfe0b5c6a 100644 --- a/www/js/onboarding/OnboardingStack.tsx +++ b/www/js/onboarding/OnboardingStack.tsx @@ -1,16 +1,15 @@ -import React, { useContext } from "react"; -import { StyleSheet } from "react-native"; -import { AppContext } from "../App"; -import WelcomePage from "./WelcomePage"; -import ProtocolPage from "./ProtocolPage"; -import SurveyPage from "./SurveyPage"; -import SaveQrPage from "./SaveQrPage"; -import SummaryPage from "./SummaryPage"; -import { OnboardingRoute } from "./onboardingHelper"; -import { displayErrorMsg } from "../plugin/logger"; +import React, { useContext } from 'react'; +import { StyleSheet } from 'react-native'; +import { AppContext } from '../App'; +import WelcomePage from './WelcomePage'; +import ProtocolPage from './ProtocolPage'; +import SurveyPage from './SurveyPage'; +import SaveQrPage from './SaveQrPage'; +import SummaryPage from './SummaryPage'; +import { OnboardingRoute } from './onboardingHelper'; +import { displayErrorMsg } from '../plugin/logger'; const OnboardingStack = () => { - const { onboardingState } = useContext(AppContext); console.debug('onboardingState in OnboardingStack', onboardingState); @@ -28,7 +27,7 @@ const OnboardingStack = () => { } else { displayErrorMsg('OnboardingStack: unknown route', onboardingState.route); } -} +}; export const onboardingStyles = StyleSheet.create({ page: { @@ -50,4 +49,4 @@ export const onboardingStyles = StyleSheet.create({ }, }); -export default OnboardingStack +export default OnboardingStack; diff --git a/www/js/onboarding/PrivacyPolicy.tsx b/www/js/onboarding/PrivacyPolicy.tsx index f237e359c..bfd884cac 100644 --- a/www/js/onboarding/PrivacyPolicy.tsx +++ b/www/js/onboarding/PrivacyPolicy.tsx @@ -1,59 +1,73 @@ -import React, { useMemo } from "react"; -import { StyleSheet, Text } from "react-native"; -import { useTranslation } from "react-i18next"; -import useAppConfig from "../useAppConfig"; -import { getTemplateText } from "./StudySummary"; +import React, { useMemo } from 'react'; +import { StyleSheet, Text } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import useAppConfig from '../useAppConfig'; +import { getTemplateText } from './StudySummary'; const PrivacyPolicy = () => { - const { t, i18n } = useTranslation(); - const appConfig = useAppConfig(); + const { t, i18n } = useTranslation(); + const appConfig = useAppConfig(); - 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, i18n.language), [appConfig]); - const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); + return ( + <> + {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 ( - <> - {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 { @@ -121,15 +139,16 @@ const PrivacyPolicy = () => { }}> {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 { @@ -137,41 +156,49 @@ const PrivacyPolicy = () => { }}> k.shankari@nrel.gov */} - (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})} - - ) -} + (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, + })} + + + ); +}; const styles = StyleSheet.create({ - hyperlinkStyle: (linkColor) => ({ - color: linkColor - }), - text: { - fontSize: 14, - }, - header: { - fontWeight: "bold", - fontSize: 18 - }, - title: { - fontWeight: "bold", - fontSize: 22, - paddingBottom: 10, - textAlign: "center" - }, - divider: { - marginVertical: 10 - } - }); + hyperlinkStyle: (linkColor) => ({ + color: linkColor, + }), + text: { + fontSize: 14, + }, + header: { + fontWeight: 'bold', + fontSize: 18, + }, + title: { + fontWeight: 'bold', + fontSize: 22, + paddingBottom: 10, + textAlign: 'center', + }, + divider: { + marginVertical: 10, + }, +}); export default PrivacyPolicy; diff --git a/www/js/onboarding/ProtocolPage.tsx b/www/js/onboarding/ProtocolPage.tsx index a047a0aae..1f096ecc9 100644 --- a/www/js/onboarding/ProtocolPage.tsx +++ b/www/js/onboarding/ProtocolPage.tsx @@ -10,7 +10,6 @@ import { markConsented } from '../splash/startprefs'; import { setProtocolDone } from './onboardingHelper'; const ProtocolPage = () => { - const { t } = useTranslation(); const context = useContext(AppContext); const { refreshOnboardingState } = context; @@ -18,25 +17,33 @@ const ProtocolPage = () => { /* If the user does not consent, we boot them back out to the join screen */ function disagree() { resetDataAndRefresh(); - }; + } function agree() { - setProtocolDone(true); - refreshOnboardingState(); - }; + setProtocolDone(true); + refreshOnboardingState(); + } // privacy policy and data collection info, followed by accept/reject buttons - return (<> - - - - - - - - - - ); -} + return ( + <> + + + + + + + + + + + ); +}; export default ProtocolPage; diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 3bfc93bb4..768fa9101 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -1,22 +1,21 @@ -import React, { useContext, useEffect, useState } from "react"; -import { View, StyleSheet } from "react-native"; -import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; -import { registerUserDone, setRegisterUserDone, setSaveQrDone } from "./onboardingHelper"; -import { AppContext } from "../App"; -import { getAngularService } from "../angular-react-helper"; -import { displayError, logDebug } from "../plugin/logger"; -import { useTranslation } from "react-i18next"; -import QrCode, { shareQR } from "../components/QrCode"; -import { onboardingStyles } from "./OnboardingStack"; -import { preloadDemoSurveyResponse } from "./SurveyPage"; -import { storageSet } from "../plugin/storage"; -import { registerUser } from "../commHelper"; -import { resetDataAndRefresh } from "../config/dynamicConfig"; -import { markConsented } from "../splash/startprefs"; -import i18next from "i18next"; - -const SaveQrPage = ({ }) => { +import React, { useContext, useEffect, useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { ActivityIndicator, Button, Surface, Text } from 'react-native-paper'; +import { registerUserDone, setRegisterUserDone, setSaveQrDone } from './onboardingHelper'; +import { AppContext } from '../App'; +import { getAngularService } from '../angular-react-helper'; +import { displayError, logDebug } from '../plugin/logger'; +import { useTranslation } from 'react-i18next'; +import QrCode, { shareQR } from '../components/QrCode'; +import { onboardingStyles } from './OnboardingStack'; +import { preloadDemoSurveyResponse } from './SurveyPage'; +import { storageSet } from '../plugin/storage'; +import { registerUser } from '../commHelper'; +import { resetDataAndRefresh } from '../config/dynamicConfig'; +import { markConsented } from '../splash/startprefs'; +import i18next from 'i18next'; +const SaveQrPage = ({}) => { const { t } = useTranslation(); const { permissionStatus, onboardingState, refreshOnboardingState } = useContext(AppContext); const { overallStatus } = permissionStatus; @@ -24,36 +23,39 @@ const SaveQrPage = ({ }) => { useEffect(() => { if (overallStatus == true && !registerUserDone) { logDebug('permissions done, going to log in'); - markConsented() - .then(login(onboardingState.opcode) - .then((response) => { - logDebug('login done, refreshing onboarding state'); - setRegisterUserDone(true); - preloadDemoSurveyResponse(); - refreshOnboardingState(); - }) - ); + markConsented().then( + login(onboardingState.opcode).then((response) => { + logDebug('login done, refreshing onboarding state'); + setRegisterUserDone(true); + preloadDemoSurveyResponse(); + refreshOnboardingState(); + }), + ); } else { logDebug('permissions not done, waiting'); } }, [overallStatus]); function login(token) { - const EXPECTED_METHOD = "prompted-auth"; - const dbStorageObject = {"token": token}; - logDebug("about to login with token"); - return storageSet(EXPECTED_METHOD, dbStorageObject).then((r) => { - registerUser().then((r) => { - logDebug("registered user in CommHelper result " + r); - refreshOnboardingState(); - }).catch((e) => { - displayError(e, "User registration error"); - resetDataAndRefresh(); + const EXPECTED_METHOD = 'prompted-auth'; + const dbStorageObject = { token: token }; + logDebug('about to login with token'); + return storageSet(EXPECTED_METHOD, dbStorageObject) + .then((r) => { + registerUser() + .then((r) => { + logDebug('registered user in CommHelper result ' + r); + refreshOnboardingState(); + }) + .catch((e) => { + displayError(e, 'User registration error'); + resetDataAndRefresh(); + }); + }) + .catch((e) => { + displayError(e, 'Sign in error'); }); - }).catch((e) => { - displayError(e, "Sign in error"); - }); - }; + } function onFinish() { setSaveQrDone(true); @@ -63,30 +65,28 @@ const SaveQrPage = ({ }) => { return ( - + {t('login.make-sure-save-your-opcode')} - + {t('login.cannot-retrieve')} - - - - {onboardingState.opcode} - + + + {onboardingState.opcode} - - ); -} +}; const s = StyleSheet.create({ opcodeText: { diff --git a/www/js/onboarding/StudySummary.tsx b/www/js/onboarding/StudySummary.tsx index 3996ba076..9913c6d81 100644 --- a/www/js/onboarding/StudySummary.tsx +++ b/www/js/onboarding/StudySummary.tsx @@ -1,45 +1,48 @@ -import React, { useMemo } from "react"; -import { View, StyleSheet } from "react-native"; -import { Text } from "react-native-paper"; -import { useTranslation } from "react-i18next"; -import useAppConfig from "../useAppConfig"; +import React, { useMemo } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Text } from 'react-native-paper'; +import { useTranslation } from 'react-i18next'; +import useAppConfig from '../useAppConfig'; export function getTemplateText(configObject, lang) { - if (configObject && (configObject.name)) { + if (configObject && configObject.name) { return configObject.intro.translated_text[lang]; } } const StudySummary = () => { - const { i18n } = useTranslation(); const appConfig = useAppConfig(); const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); - return (<> - {templateText?.deployment_name} - {appConfig?.intro?.deployment_partner_name + " " + templateText?.deployment_name} - - {"✔️ " + templateText?.summary_line_1} - {"✔️ " + templateText?.summary_line_2} - {"✔️ " + templateText?.summary_line_3} - - ) + return ( + <> + {templateText?.deployment_name} + + {appConfig?.intro?.deployment_partner_name + ' ' + templateText?.deployment_name} + + + {'✔️ ' + templateText?.summary_line_1} + {'✔️ ' + templateText?.summary_line_2} + {'✔️ ' + templateText?.summary_line_3} + + + ); }; const styles = StyleSheet.create({ title: { - fontWeight: "bold", + fontWeight: 'bold', fontSize: 24, paddingBottom: 10, - textAlign: "center" + textAlign: 'center', }, text: { fontSize: 15, }, studyName: { - fontWeight: "bold", + fontWeight: 'bold', fontSize: 17, }, }); diff --git a/www/js/onboarding/SummaryPage.tsx b/www/js/onboarding/SummaryPage.tsx index d15e9f60e..7acd1d1be 100644 --- a/www/js/onboarding/SummaryPage.tsx +++ b/www/js/onboarding/SummaryPage.tsx @@ -8,7 +8,6 @@ import StudySummary from './StudySummary'; import { setSummaryDone } from './onboardingHelper'; const SummaryPage = () => { - const { t } = useTranslation(); const context = useContext(AppContext); const { refreshOnboardingState } = context; @@ -16,21 +15,26 @@ const SummaryPage = () => { function next() { setSummaryDone(true); refreshOnboardingState(); - }; + } // summary of the study, followed by 'next' button - return (<> - - - - - - - - - - - ); -} + return ( + <> + + + + + + + + + + + + ); +}; export default SummaryPage; diff --git a/www/js/onboarding/SurveyPage.tsx b/www/js/onboarding/SurveyPage.tsx index c02439cbf..3ba430e85 100644 --- a/www/js/onboarding/SurveyPage.tsx +++ b/www/js/onboarding/SurveyPage.tsx @@ -1,16 +1,19 @@ -import React, { useState, useEffect, useContext, useMemo } from "react"; -import { View, StyleSheet } from "react-native"; -import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; -import EnketoModal from "../survey/enketo/EnketoModal"; -import { DEMOGRAPHIC_SURVEY_DATAKEY, DEMOGRAPHIC_SURVEY_NAME } from "../control/DemographicsSettingRow"; -import { loadPreviousResponseForSurvey } from "../survey/enketo/enketoHelper"; -import { AppContext } from "../App"; -import { markIntroDone, registerUserDone } from "./onboardingHelper"; -import { useTranslation } from "react-i18next"; -import { DateTime } from "luxon"; -import { onboardingStyles } from "./OnboardingStack"; -import { displayErrorMsg } from "../plugin/logger"; -import i18next from "i18next"; +import React, { useState, useEffect, useContext, useMemo } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { ActivityIndicator, Button, Surface, Text } from 'react-native-paper'; +import EnketoModal from '../survey/enketo/EnketoModal'; +import { + DEMOGRAPHIC_SURVEY_DATAKEY, + DEMOGRAPHIC_SURVEY_NAME, +} from '../control/DemographicsSettingRow'; +import { loadPreviousResponseForSurvey } from '../survey/enketo/enketoHelper'; +import { AppContext } from '../App'; +import { markIntroDone, registerUserDone } from './onboardingHelper'; +import { useTranslation } from 'react-i18next'; +import { DateTime } from 'luxon'; +import { onboardingStyles } from './OnboardingStack'; +import { displayErrorMsg } from '../plugin/logger'; +import i18next from 'i18next'; let preloadedResponsePromise: Promise = null; export const preloadDemoSurveyResponse = () => { @@ -22,10 +25,9 @@ export const preloadDemoSurveyResponse = () => { preloadedResponsePromise = loadPreviousResponseForSurvey(DEMOGRAPHIC_SURVEY_DATAKEY); } return preloadedResponsePromise; -} +}; const SurveyPage = () => { - const { t } = useTranslation(); const { refreshOnboardingState } = useContext(AppContext); const [surveyModalVisible, setSurveyModalVisible] = useState(false); @@ -33,7 +35,7 @@ const SurveyPage = () => { const prevSurveyResponseDate = useMemo(() => { if (prevSurveyResponse) { const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(prevSurveyResponse, "text/xml"); + const xmlDoc = parser.parseFromString(prevSurveyResponse, 'text/xml'); const surveyEndDt = xmlDoc.querySelector('end')?.textContent; // ISO datetime of survey completion return DateTime.fromISO(surveyEndDt).toLocaleString(DateTime.DATE_FULL); } @@ -60,42 +62,49 @@ const SurveyPage = () => { refreshOnboardingState(); } - return (<> - - {prevSurveyResponse ? - - - {t('survey.prev-survey-found')} - {prevSurveyResponseDate} + return ( + <> + + {prevSurveyResponse ? ( + + + + {' '} + {t('survey.prev-survey-found')}{' '} + + {prevSurveyResponseDate} + + + + + - - - + ) : ( + + + {t('survey.loading-prior-survey')} - - : - - - - {t('survey.loading-prior-survey')} - - - } - - setSurveyModalVisible(false)} - onResponseSaved={onFinish} surveyName={DEMOGRAPHIC_SURVEY_NAME} - opts={{ - /* If there is no prev response, we need an initial response from the user and should + )} + + setSurveyModalVisible(false)} + onResponseSaved={onFinish} + surveyName={DEMOGRAPHIC_SURVEY_NAME} + opts={{ + /* If there is no prev response, we need an initial response from the user and should not allow them to dismiss the modal by the "<- Dismiss" button */ - undismissable: !prevSurveyResponse, - prefilledSurveyResponse: prevSurveyResponse, - dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, - }} /> - ); + undismissable: !prevSurveyResponse, + prefilledSurveyResponse: prevSurveyResponse, + dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, + }} + /> + + ); }; export default SurveyPage; diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index cb317c5bc..7c09a21d3 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -1,16 +1,33 @@ import React, { useContext, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { View, Image, Modal, ScrollView, StyleSheet, ViewStyle, useWindowDimensions } from 'react-native'; -import { Button, Dialog, Divider, IconButton, Surface, Text, TextInput, TouchableRipple, useTheme } from 'react-native-paper'; +import { + View, + Image, + Modal, + ScrollView, + StyleSheet, + ViewStyle, + useWindowDimensions, +} from 'react-native'; +import { + Button, + Dialog, + Divider, + IconButton, + Surface, + Text, + TextInput, + TouchableRipple, + useTheme, +} from 'react-native-paper'; import color from 'color'; import { initByUser } from '../config/dynamicConfig'; import { AppContext } from '../App'; -import { displayError, logDebug } from "../plugin/logger"; +import { displayError, logDebug } from '../plugin/logger'; import { onboardingStyles } from './OnboardingStack'; import { Icon } from '../components/Icon'; const WelcomePage = () => { - const { t } = useTranslation(); const { colors } = useTheme(); const { width: windowWidth } = useWindowDimensions(); @@ -23,128 +40,158 @@ const WelcomePage = () => { const getCode = function (result) { let url = new window.URL(result.text); let notCancelled = result.cancelled == false; - let isQR = result.format == "QR_CODE"; - let hasPrefix = url.protocol == "emission:"; - let hasToken = url.searchParams.has("token"); - let code = url.searchParams.get("token"); + let isQR = result.format == 'QR_CODE'; + let hasPrefix = url.protocol == 'emission:'; + let hasToken = url.searchParams.has('token'); + let code = url.searchParams.get('token'); - logDebug("QR code " + result.text + " checks: cancel, format, prefix, params, code " + notCancelled + isQR + hasPrefix + hasToken + code); + logDebug( + 'QR code ' + + result.text + + ' checks: cancel, format, prefix, params, code ' + + notCancelled + + isQR + + hasPrefix + + hasToken + + code, + ); if (notCancelled && isQR && hasPrefix && hasToken) { return code; } else { return false; } - }; + }; const scanCode = function () { window['cordova'].plugins.barcodeScanner.scan( function (result) { - console.debug("scanned code", result); + console.debug('scanned code', result); let code = getCode(result); if (code != false) { - console.log("found code", code); + console.log('found code', code); loginWithToken(code); } else { - displayError(result.text, "invalid study reference"); + displayError(result.text, 'invalid study reference'); } }, function (error) { - displayError(error, "Scanning failed: "); - }); + displayError(error, 'Scanning failed: '); + }, + ); }; function loginWithToken(token) { - initByUser({token}).then((configUpdated) => { - if (configUpdated) { - setPasteModalVis(false); - refreshOnboardingState(); - } - }).catch(err => { - console.error('Error logging in with token', err); - setExistingToken(''); - }); + initByUser({ token }) + .then((configUpdated) => { + if (configUpdated) { + setPasteModalVis(false); + refreshOnboardingState(); + } + }) + .catch((err) => { + console.error('Error logging in with token', err); + setExistingToken(''); + }); } - return (<> - - - setInfoPopupVis(true)} /> - - + return ( + <> + + + setInfoPopupVis(true)} + /> + + + + + + }} + /> + + + {t('join.to-proceed-further')} + {t('join.code-hint')} + + + + + {t('join.scan-code')} + + {t('join.scan-hint')} + + + + setPasteModalVis(true)} icon="content-paste"> + {t('join.paste-code')} + + {t('join.paste-hint')} + + + - - - }} /> - - - {t('join.to-proceed-further')} - {t('join.code-hint')} - - - - - {t('join.scan-code')} - - {t('join.scan-hint')} - - - - setPasteModalVis(true)} icon='content-paste'> - {t('join.paste-code')} - - {t('join.paste-hint')} - - - - - setPasteModalVis(false)}> - setPasteModalVis(false)}> - - - - - - - - setInfoPopupVis(false)}> - setInfoPopupVis(false)}> - - {t('join.about-app-title', {appName: t('join.app-name')})} - - - - {t('join.about-app-para-1')} - {t('join.about-app-para-2')} - {t('join.about-app-para-3')} - {t('join.tips-title')} - - {t('join.all-green-status')} - - {t('join.dont-force-kill')} - - {t('join.background-restrictions')} - - - - - - - - ); -} + setPasteModalVis(false)}> + setPasteModalVis(false)}> + + + + + + + + setInfoPopupVis(false)}> + setInfoPopupVis(false)}> + {t('join.about-app-title', { appName: t('join.app-name') })} + + + {t('join.about-app-para-1')} + {t('join.about-app-para-2')} + {t('join.about-app-para-3')} + {t('join.tips-title')} + - {t('join.all-green-status')} + - {t('join.dont-force-kill')} + - {t('join.background-restrictions')} + + + + + + + + + ); +}; const s: any = StyleSheet.create({ headerArea: ((windowWidth, colors) => ({ width: windowWidth * 2.5, height: windowWidth, - left: -windowWidth * .75, + left: -windowWidth * 0.75, borderBottomRightRadius: '50%', borderBottomLeftRadius: '50%', position: 'absolute', - top: windowWidth * -2/3, + top: (windowWidth * -2) / 3, backgroundColor: colors.primary, boxShadow: `0 16px ${color(colors.primary).alpha(0.3).rgb().string()}`, })) as ViewStyle, @@ -183,9 +230,7 @@ const s: any = StyleSheet.create({ }, }); - const WelcomePageButton = ({ onPress, icon, children }) => { - const { colors } = useTheme(); const { width: windowWidth } = useWindowDimensions(); @@ -193,13 +238,13 @@ const WelcomePageButton = ({ onPress, icon, children }) => { - + {children} ); -} +}; const welcomeButtonStyles: any = StyleSheet.create({ btn: ((colors): ViewStyle => ({ diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index 4110c2394..b776d65bd 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -1,9 +1,9 @@ -import { DateTime } from "luxon"; -import { getConfig, resetDataAndRefresh } from "../config/dynamicConfig"; -import { storageGet, storageSet } from "../plugin/storage"; -import { logDebug } from "../plugin/logger"; -import { readConsentState, isConsented } from "../splash/startprefs"; -import { getAngularService } from "../angular-react-helper"; +import { DateTime } from 'luxon'; +import { getConfig, resetDataAndRefresh } from '../config/dynamicConfig'; +import { storageGet, storageSet } from '../plugin/storage'; +import { logDebug } from '../plugin/logger'; +import { readConsentState, isConsented } from '../splash/startprefs'; +import { getAngularService } from '../angular-react-helper'; export const INTRO_DONE_KEY = 'intro_done'; @@ -13,53 +13,70 @@ export const INTRO_DONE_KEY = 'intro_done'; // route = SAVE_QR if config present, protocol done, but save qr not done // route = SURVEY if config present, consented and save qr done // route = DONE if onboarding is finished (intro_done marked) -export enum OnboardingRoute { WELCOME, SUMMARY, PROTOCOL, SAVE_QR, SURVEY, DONE }; -export type OnboardingState = { - opcode: string, - route: OnboardingRoute, +export enum OnboardingRoute { + WELCOME, + SUMMARY, + PROTOCOL, + SAVE_QR, + SURVEY, + DONE, } +export type OnboardingState = { + opcode: string; + route: OnboardingRoute; +}; export let summaryDone = false; -export const setSummaryDone = (b) => summaryDone = b; +export const setSummaryDone = (b) => (summaryDone = b); export let protocolDone = false; -export const setProtocolDone = (b) => protocolDone = b; +export const setProtocolDone = (b) => (protocolDone = b); export let saveQrDone = false; -export const setSaveQrDone = (b) => saveQrDone = b; +export const setSaveQrDone = (b) => (saveQrDone = b); export let registerUserDone = false; -export const setRegisterUserDone = (b) => registerUserDone = b; +export const setRegisterUserDone = (b) => (registerUserDone = b); export function getPendingOnboardingState(): Promise { - return Promise.all([getConfig(), readConsented(), readIntroDone()]).then(([config, isConsented, isIntroDone]) => { - let route: OnboardingRoute; + return Promise.all([getConfig(), readConsented(), readIntroDone()]).then( + ([config, isConsented, isIntroDone]) => { + let route: OnboardingRoute; - // backwards compat - prev. versions might have config cleared but still have intro_done set - if (!config && (isIntroDone || isConsented)) { - resetDataAndRefresh(); // if there's no config, we need to reset everything - return null; - } - - if (isIntroDone) { - route = OnboardingRoute.DONE; - } else if (!config) { - route = OnboardingRoute.WELCOME; - } else if (!protocolDone && !summaryDone) { - route = OnboardingRoute.SUMMARY; - } else if (!protocolDone) { - route = OnboardingRoute.PROTOCOL; - } else if (!saveQrDone) { - route = OnboardingRoute.SAVE_QR; - } else { - route = OnboardingRoute.SURVEY; - } + // backwards compat - prev. versions might have config cleared but still have intro_done set + if (!config && (isIntroDone || isConsented)) { + resetDataAndRefresh(); // if there's no config, we need to reset everything + return null; + } - logDebug("pending onboarding state is " + route + " intro, config, consent, qr saved : " + isIntroDone + config + isConsented + saveQrDone); + if (isIntroDone) { + route = OnboardingRoute.DONE; + } else if (!config) { + route = OnboardingRoute.WELCOME; + } else if (!protocolDone && !summaryDone) { + route = OnboardingRoute.SUMMARY; + } else if (!protocolDone) { + route = OnboardingRoute.PROTOCOL; + } else if (!saveQrDone) { + route = OnboardingRoute.SAVE_QR; + } else { + route = OnboardingRoute.SURVEY; + } - return { route, opcode: config?.joined?.opcode }; - }); -}; + logDebug( + 'pending onboarding state is ' + + route + + ' intro, config, consent, qr saved : ' + + isIntroDone + + config + + isConsented + + saveQrDone, + ); + + return { route, opcode: config?.joined?.opcode }; + }, + ); +} async function readConsented() { return readConsentState().then(isConsented) as Promise; @@ -71,13 +88,12 @@ export async function readIntroDone() { export async function markIntroDone() { const currDateTime = DateTime.now().toISO(); - return storageSet(INTRO_DONE_KEY, currDateTime) - .then(() => { - //handle "on intro" events - logDebug("intro done, calling registerPush and storeDeviceSettings"); - const PushNotify = getAngularService("PushNotify"); - const StoreSeviceSettings = getAngularService("StoreDeviceSettings"); - PushNotify.registerPush(); - StoreSeviceSettings.storeDeviceSettings(); - }); + return storageSet(INTRO_DONE_KEY, currDateTime).then(() => { + //handle "on intro" events + logDebug('intro done, calling registerPush and storeDeviceSettings'); + const PushNotify = getAngularService('PushNotify'); + const StoreSeviceSettings = getAngularService('StoreDeviceSettings'); + PushNotify.registerPush(); + StoreSeviceSettings.storeDeviceSettings(); + }); } diff --git a/www/js/plugin/clientStats.ts b/www/js/plugin/clientStats.ts index cefaf8f22..6735ef5ff 100644 --- a/www/js/plugin/clientStats.ts +++ b/www/js/plugin/clientStats.ts @@ -1,24 +1,24 @@ -import { displayErrorMsg } from "./logger"; +import { displayErrorMsg } from './logger'; -const CLIENT_TIME = "stats/client_time"; -const CLIENT_ERROR = "stats/client_error"; -const CLIENT_NAV_EVENT = "stats/client_nav_event"; +const CLIENT_TIME = 'stats/client_time'; +const CLIENT_ERROR = 'stats/client_error'; +const CLIENT_NAV_EVENT = 'stats/client_nav_event'; export const statKeys = { - 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" + 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', }; let appVersion; @@ -28,32 +28,32 @@ export const getAppVersion = () => { appVersion = version; return version; }); -} +}; const getStatsEvent = async (name: string, reading: any) => { const ts = Date.now() / 1000; const client_app_version = await getAppVersion(); const client_os_version = window['device'].version; return { name, ts, reading, client_app_version, client_os_version }; -} +}; export const addStatReading = async (name: string, reading: any) => { const db = window['cordova']?.plugins?.BEMUserCache; const event = await getStatsEvent(name, reading); if (db) return db.putMessage(CLIENT_TIME, event); - displayErrorMsg("addStatReading: db is not defined"); -} + displayErrorMsg('addStatReading: db is not defined'); +}; export const addStatEvent = async (name: string) => { const db = window['cordova']?.plugins?.BEMUserCache; const event = await getStatsEvent(name, null); if (db) return db.putMessage(CLIENT_NAV_EVENT, event); - displayErrorMsg("addStatEvent: db is not defined"); -} + displayErrorMsg('addStatEvent: db is not defined'); +}; export const addStatError = async (name: string, errorStr: string) => { const db = window['cordova']?.plugins?.BEMUserCache; const event = await getStatsEvent(name, errorStr); if (db) return db.putMessage(CLIENT_ERROR, event); - displayErrorMsg("addStatError: db is not defined"); -} + displayErrorMsg('addStatError: db is not defined'); +}; diff --git a/www/js/plugin/logger.ts b/www/js/plugin/logger.ts index d127f5549..376c6486b 100644 --- a/www/js/plugin/logger.ts +++ b/www/js/plugin/logger.ts @@ -1,28 +1,33 @@ 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); @@ -40,8 +45,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.ts b/www/js/plugin/storage.ts index 643e985e1..63604e8c1 100644 --- a/www/js/plugin/storage.ts +++ b/www/js/plugin/storage.ts @@ -1,15 +1,15 @@ -import { getAngularService } from "../angular-react-helper"; -import { addStatReading, statKeys } from "./clientStats"; -import { logDebug, logWarn } from "./logger"; +import { getAngularService } from '../angular-react-helper'; +import { addStatReading, statKeys } from './clientStats'; +import { logDebug, logWarn } from './logger'; const mungeValue = (key, value) => { let store_val = value; - if (typeof value != "object") { + if (typeof value != 'object') { store_val = {}; store_val[key] = value; } return store_val; -} +}; /* * If a non-JSON object was munged for storage, unwrap it. @@ -22,16 +22,16 @@ const unmungeValue = (key, retData) => { // it must have been an object return retData; } -} +}; -const localStorageSet = (key: string, value: {[k: string]: any}) => { +const localStorageSet = (key: string, value: { [k: string]: any }) => { //checking for a value to prevent storing undefined //case where local was null and native was undefined stored "undefined" //see discussion: https://github.com/e-mission/e-mission-phone/pull/1072#discussion_r1373753945 if (value) { localStorage.setItem(key, JSON.stringify(value)); } -} +}; const localStorageGet = (key: string) => { const value = localStorage.getItem(key); @@ -40,7 +40,7 @@ const localStorageGet = (key: string) => { } else { return null; } -} +}; /* We redundantly store data in both local and native storage. This function checks both for a value. If a value is present in only one, it copies it to the other and returns it. @@ -48,47 +48,55 @@ const localStorageGet = (key: string) => { local storage and returns it. */ function getUnifiedValue(key) { const ls_stored_val = localStorageGet(key); - return window['cordova'].plugins.BEMUserCache.getLocalStorage(key, false).then((uc_stored_val) => { - logDebug(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + return window['cordova'].plugins.BEMUserCache.getLocalStorage(key, false).then( + (uc_stored_val) => { + logDebug(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, ls_stored_val = ${JSON.stringify(ls_stored_val)}.`); - /* compare stored values by stringified JSON equality, not by == or ===. + /* compare stored values by stringified JSON equality, not by == or ===. for objects, == or === only compares the references, not the contents of the objects */ - if (JSON.stringify(ls_stored_val) == JSON.stringify(uc_stored_val)) { - logDebug("local and native values match, already synced"); - return uc_stored_val; - } else { - // the values are different - if (ls_stored_val == null) { - // local value is missing, fill it in from native - console.assert(uc_stored_val != null, "uc_stored_val should be non-null"); - logWarn(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + if (JSON.stringify(ls_stored_val) == JSON.stringify(uc_stored_val)) { + logDebug('local and native values match, already synced'); + return uc_stored_val; + } else { + // the values are different + if (ls_stored_val == null) { + // local value is missing, fill it in from native + console.assert(uc_stored_val != null, 'uc_stored_val should be non-null'); + logWarn(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, ls_stored_val = ${JSON.stringify(ls_stored_val)}. copying native ${key} to local...`); - localStorageSet(key, uc_stored_val); - return uc_stored_val; - } else if (uc_stored_val == null) { - // native value is missing, fill it in from local - console.assert(ls_stored_val != null); - logWarn(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + localStorageSet(key, uc_stored_val); + return uc_stored_val; + } else if (uc_stored_val == null) { + // native value is missing, fill it in from local + console.assert(ls_stored_val != null); + logWarn(`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 window['cordova'].plugins.BEMUserCache.putLocalStorage(key, ls_stored_val).then(() => { - // we only return the value after we have finished writing - return ls_stored_val; - }); - } - // both values are present, but they are different - 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)); - logWarn(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + return window['cordova'].plugins.BEMUserCache.putLocalStorage(key, ls_stored_val).then( + () => { + // we only return the value after we have finished writing + return ls_stored_val; + }, + ); + } + // both values are present, but they are different + 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), + ); + logWarn(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, ls_stored_val = ${JSON.stringify(ls_stored_val)}. copying native ${key} to local...`); - localStorageSet(key, uc_stored_val); - return uc_stored_val; - } - }); + localStorageSet(key, uc_stored_val); + return uc_stored_val; + } + }, + ); } export function storageSet(key: string, value: any) { @@ -112,7 +120,7 @@ export function storageRemove(key: string) { return window['cordova'].plugins.BEMUserCache.removeLocalStorage(key); } -export function storageClear({ local, native }: { local?: boolean, native?: boolean }) { +export function storageClear({ local, native }: { local?: boolean; native?: boolean }) { if (local) localStorage.clear(); if (native) return window['cordova'].plugins.BEMUserCache.clearAll(); return Promise.resolve(); @@ -138,42 +146,51 @@ function findMissing(fromKeys, toKeys) { } export function storageSyncLocalAndNative() { - console.log("STORAGE_PLUGIN: Called syncAllWebAndNativeValues "); - const syncKeys = window['cordova'].plugins.BEMUserCache.listAllLocalStorageKeys().then((nativeKeys) => { - console.log("STORAGE_PLUGIN: native plugin returned"); - const webKeys = Object.keys(localStorage); - // 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 - logDebug("STORAGE_PLUGIN: Comparing web keys " + webKeys + " with " + nativeKeys); - let [foundNative, missingNative] = findMissing(webKeys, nativeKeys); - let [foundWeb, missingWeb] = findMissing(nativeKeys, webKeys); - logDebug("STORAGE_PLUGIN: Found native keys " + foundNative + " missing native keys " + missingNative); - logDebug("STORAGE_PLUGIN: Found web keys " + foundWeb + " missing web keys " + missingWeb); - const allMissing = missingNative.concat(missingWeb); - logDebug("STORAGE_PLUGIN: Syncing all missing keys " + allMissing); - allMissing.forEach(getUnifiedValue); - if (allMissing.length != 0) { - addStatReading(statKeys.MISSING_KEYS, { - "type": "local_storage_mismatch", - "allMissingLength": allMissing.length, - "missingWebLength": missingWeb.length, - "missingNativeLength": missingNative.length, - "foundWebLength": foundWeb.length, - "foundNativeLength": foundNative.length, - "allMissing": allMissing, - }).then(logDebug("Logged missing keys to client stats")); - } - }); - const listAllKeys = window['cordova'].plugins.BEMUserCache.listAllUniqueKeys().then((nativeKeys) => { - logDebug("STORAGE_PLUGIN: For the record, all unique native keys are " + nativeKeys); - if (nativeKeys.length == 0) { - addStatReading(statKeys.MISSING_KEYS, { - "type": "all_native", - }).then(logDebug("Logged all missing native keys to client stats")); - } - }); + console.log('STORAGE_PLUGIN: Called syncAllWebAndNativeValues '); + const syncKeys = window['cordova'].plugins.BEMUserCache.listAllLocalStorageKeys().then( + (nativeKeys) => { + console.log('STORAGE_PLUGIN: native plugin returned'); + const webKeys = Object.keys(localStorage); + // 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 + logDebug('STORAGE_PLUGIN: Comparing web keys ' + webKeys + ' with ' + nativeKeys); + let [foundNative, missingNative] = findMissing(webKeys, nativeKeys); + let [foundWeb, missingWeb] = findMissing(nativeKeys, webKeys); + logDebug( + 'STORAGE_PLUGIN: Found native keys ' + + foundNative + + ' missing native keys ' + + missingNative, + ); + logDebug('STORAGE_PLUGIN: Found web keys ' + foundWeb + ' missing web keys ' + missingWeb); + const allMissing = missingNative.concat(missingWeb); + logDebug('STORAGE_PLUGIN: Syncing all missing keys ' + allMissing); + allMissing.forEach(getUnifiedValue); + if (allMissing.length != 0) { + addStatReading(statKeys.MISSING_KEYS, { + type: 'local_storage_mismatch', + allMissingLength: allMissing.length, + missingWebLength: missingWeb.length, + missingNativeLength: missingNative.length, + foundWebLength: foundWeb.length, + foundNativeLength: foundNative.length, + allMissing: allMissing, + }).then(logDebug('Logged missing keys to client stats')); + } + }, + ); + const listAllKeys = window['cordova'].plugins.BEMUserCache.listAllUniqueKeys().then( + (nativeKeys) => { + logDebug('STORAGE_PLUGIN: For the record, all unique native keys are ' + nativeKeys); + if (nativeKeys.length == 0) { + addStatReading(statKeys.MISSING_KEYS, { + type: 'all_native', + }).then(logDebug('Logged all missing native keys to client stats')); + } + }, + ); return Promise.all([syncKeys, listAllKeys]); } diff --git a/www/js/services.js b/www/js/services.js index 0c9c6e2ac..444ff94b7 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -3,30 +3,39 @@ import angular from 'angular'; import { getRawEntries } from './commHelper'; -angular.module('emission.services', ['emission.plugin.logger']) +angular + .module('emission.services', ['emission.plugin.logger']) -.service('ReferHelper', function($http) { - - this.habiticaRegister = function(groupid, successCallback, errorCallback) { - window.cordova.plugins.BEMServerComm.getUserPersonalData("/join.group/"+groupid, successCallback, errorCallback); + .service('ReferHelper', function ($http) { + this.habiticaRegister = function (groupid, successCallback, errorCallback) { + window.cordova.plugins.BEMServerComm.getUserPersonalData( + '/join.group/' + groupid, + successCallback, + errorCallback, + ); }; - this.joinGroup = function(groupid, userid) { - - // TODO: - return new Promise(function(resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData("/join.group/"+groupid, "inviter", userid, resolve, reject); - }) + 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, Logger) { - var combineWithDedup = function(list1, list2) { + //function firstUpperCase(string) { + // return string[0].toUpperCase() + string.slice(1); + //}*/ + }; + }) + .service('UnifiedDataLoader', function ($window, Logger) { + var combineWithDedup = function (list1, list2) { var combinedList = list1.concat(list2); - return combinedList.filter(function(value, i, array) { - var firstIndexOfValue = array.findIndex(function(element, index, array) { + return 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; @@ -34,259 +43,296 @@ angular.module('emission.services', ['emission.plugin.logger']) }; // TODO: generalize to iterable of promises - var combinedPromise = function(localPromise, remotePromise, combiner) { - return new Promise(function(resolve, reject) { - var localResult = []; - var localError = null; + var combinedPromise = function (localPromise, remotePromise, combiner) { + return new Promise(function (resolve, reject) { + var localResult = []; + var localError = null; - var remoteResult = []; - var remoteError = null; + var remoteResult = []; + var remoteError = null; - var localPromiseDone = false; - var remotePromiseDone = false; + 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 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); + 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); - }) - } + remotePromise + .then( + function (currentRemoteResult) { + remoteResult = currentRemoteResult; + remotePromiseDone = true; + }, + function (error) { + remoteResult = []; + remoteError = error; + remotePromiseDone = true; + }, + ) + .then(checkAndResolve); + }); + }; // TODO: Generalize this to work for both sensor data and messages // Do we even need to separate the two kinds of data? // Alternatively, we can maintain another mapping between key -> type // Probably in www/json... - this.getUnifiedSensorDataForInterval = function(key, tq) { - var localPromise = $window.cordova.plugins.BEMUserCache.getSensorDataForInterval(key, tq, true); - var remotePromise = getRawEntries([key], tq.startTs, tq.endTs) - .then(function(serverResponse) { - return serverResponse.phone_data; - }); - return combinedPromise(localPromise, remotePromise, combineWithDedup); + this.getUnifiedSensorDataForInterval = function (key, tq) { + var localPromise = $window.cordova.plugins.BEMUserCache.getSensorDataForInterval( + key, + tq, + true, + ); + var remotePromise = getRawEntries([key], tq.startTs, tq.endTs).then( + function (serverResponse) { + return serverResponse.phone_data; + }, + ); + return combinedPromise(localPromise, remotePromise, combineWithDedup); }; - this.getUnifiedMessagesForInterval = function(key, tq, withMetadata) { + this.getUnifiedMessagesForInterval = function (key, tq, withMetadata) { var localPromise = $window.cordova.plugins.BEMUserCache.getMessagesForInterval(key, tq, true); - var remotePromise = getRawEntries([key], tq.startTs, tq.endTs) - .then(function(serverResponse) { - return serverResponse.phone_data; - }); + var remotePromise = getRawEntries([key], tq.startTs, tq.endTs).then( + function (serverResponse) { + return serverResponse.phone_data; + }, + ); return combinedPromise(localPromise, remotePromise, combineWithDedup); - } -}) -.service('ControlHelper', function($window, - $ionicPopup, - Logger) { - - this.writeFile = function(fileEntry, resultList) { + }; + }) + .service('ControlHelper', function ($window, $ionicPopup, 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); + 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); - }; + 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(); - }; + 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' }); - fileWriter.write(dataObj); - }); - // this.writeFile(fileEntry, resultList); + // 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); }); - } - + }); + }); + }; - 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(); + 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()); + 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')); } - reader.readAsText(file); - }, function(error) { - $ionicPopup.alert({title: "Error while downloading JSON dump", - template: error}); - reject(error); + 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, }); - }); - }); + reject(error); + }, + ); }); - }; + }); + }); + }; - 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); - }) + 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 + .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' - }]; + // 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 { + 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.ts b/www/js/splash/customURL.ts index bc3d93f3e..d351fcc0b 100644 --- a/www/js/splash/customURL.ts +++ b/www/js/splash/customURL.ts @@ -1,22 +1,24 @@ type UrlComponents = { - [key : string] : string -} - -type OnLaunchCustomURL = (rawUrl: string, callback: (url: string, urlComponents: UrlComponents) => void ) => void; + [key: string]: string; +}; +type OnLaunchCustomURL = ( + rawUrl: string, + callback: (url: string, urlComponents: UrlComponents) => void, +) => void; export const onLaunchCustomURL: OnLaunchCustomURL = (rawUrl, handler) => { - try { - const url = rawUrl.split('//')[1]; - const [ route, paramString ] = url.split('?'); - const paramsList = paramString.split('&'); - const urlComponents: UrlComponents = { route : route }; - for (let i = 0; i < paramsList.length; i++) { - const [key, value] = paramsList[i].split('='); - urlComponents[key] = value; - } - handler(url, urlComponents); - }catch { - console.log('not a valid url'); + try { + const url = rawUrl.split('//')[1]; + const [route, paramString] = url.split('?'); + const paramsList = paramString.split('&'); + const urlComponents: UrlComponents = { route: route }; + for (let i = 0; i < paramsList.length; i++) { + const [key, value] = paramsList[i].split('='); + urlComponents[key] = value; } -}; \ No newline at end of file + handler(url, urlComponents); + } catch { + console.log('not a valid url'); + } +}; diff --git a/www/js/splash/localnotify.js b/www/js/splash/localnotify.js index c96ba827d..9f3db3ab3 100644 --- a/www/js/splash/localnotify.js +++ b/www/js/splash/localnotify.js @@ -7,102 +7,132 @@ import angular from 'angular'; -angular.module('emission.splash.localnotify', ['emission.plugin.logger', - 'ionic-toast']) -.factory('LocalNotify', function($window, $ionicPlatform, $ionicPopup, - $state, $rootScope, ionicToast, Logger) { - var localNotify = {}; +angular + .module('emission.splash.localnotify', ['emission.plugin.logger', '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; + 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, + ); } - }, { - 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.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 22f8407ee..9ceb0a23e 100644 --- a/www/js/splash/notifScheduler.js +++ b/www/js/splash/notifScheduler.js @@ -5,12 +5,10 @@ import { getConfig } from '../config/dynamicConfig'; import { addStatReading, statKeys } from '../plugin/clientStats'; import { getUser, updateUser } from '../commHelper'; -angular.module('emission.splash.notifscheduler', - ['emission.services', - 'emission.plugin.logger']) - -.factory('NotificationScheduler', function($http, $window, $ionicPlatform, Logger) { +angular + .module('emission.splash.notifscheduler', ['emission.services', 'emission.plugin.logger']) + .factory('NotificationScheduler', function ($http, $window, $ionicPlatform, Logger) { const scheduler = {}; let _config; let scheduledPromise = new Promise((rs) => rs()); @@ -18,36 +16,36 @@ angular.module('emission.splash.notifscheduler', // like python range() function range(start, stop, step) { - let a = [start], b = start; - while (b < stop) - a.push(b += step || 1); - return a; + 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) => { - 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); - } + 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); } - return notifTimes; - } + } + return notifTimes; + }; // 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; - } + for (const t of expectedTimes) { + if (!notifs.some((n) => moment(n.at).isSame(t))) { + return false; } - return true; - } + } + return true; + }; /* remove notif actions as they do not work, can restore post routing migration */ // const setUpActions = () => { @@ -62,155 +60,155 @@ angular.module('emission.splash.notifscheduler', // } 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}`); + 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}`, + ); + }); } //new method to fetch notifications - scheduler.getScheduledNotifs = function() { - return new Promise((resolve, reject) => { - /* if the notifications are still in active scheduling it causes problems + scheduler.getScheduledNotifs = function () { + return new Promise((resolve, reject) => { + /* if the notifications are still in active scheduling it causes problems anywhere from 0-n of the scheduled notifs are displayed if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors */ - if(isScheduling) - { - console.log("requesting fetch while still actively scheduling, waiting on scheduledPromise"); - scheduledPromise.then(() => { - getNotifs().then((notifs) => { - console.log("done scheduling notifs", notifs); - resolve(notifs); - }) - }) - } - else{ - getNotifs().then((notifs) => { - resolve(notifs); - }) - } - }) - } + if (isScheduling) { + console.log( + 'requesting fetch while still actively scheduling, waiting on scheduledPromise', + ); + scheduledPromise.then(() => { + getNotifs().then((notifs) => { + console.log('done scheduling notifs', notifs); + resolve(notifs); + }); + }); + } else { + getNotifs().then((notifs) => { + resolve(notifs); + }); + } + }); + }; //get scheduled notifications from cordova plugin and format them - const getNotifs = function() { - return new Promise((resolve, reject) => { - cordova.plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length){ - console.log("there are no notifications"); - resolve([]); //if none, return empty array - } - - const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing - let scheduledNotifs = []; - scheduledNotifs = notifSubset.map((n) => { - const time = moment(n.trigger.at).format('LT'); - const date = moment(n.trigger.at).format('LL'); - return { - key: date, - val: time - } - }); - resolve(scheduledNotifs); - }); - }) - } + const getNotifs = function () { + return new Promise((resolve, reject) => { + cordova.plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length) { + console.log('there are no notifications'); + resolve([]); //if none, return empty array + } + + const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing + let scheduledNotifs = []; + scheduledNotifs = notifSubset.map((n) => { + const time = moment(n.trigger.at).format('LT'); + const date = moment(n.trigger.at).format('LL'); + return { + key: date, + val: time, + }; + }); + resolve(scheduledNotifs); + }); + }); + }; // schedules the notifications using the cordova plugin const scheduleNotifs = (scheme, notifTimes) => { - return new Promise((rs) => { - 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 - }); - }); + 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 + }); + }); + }); + }; // 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); + 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") - } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); - //enforcing end of scheduling to conisder update through - scheduledPromise.then(() => { - resolve(); - }) - } - }); - } + return new Promise((resolve, reject) => { + cordova.plugins.notification.local.getScheduled((notifs) => { + if (areAlreadyScheduled(notifs, notifTimes)) { + Logger.log('Already scheduled, not scheduling again'); + } else { + // to ensure we don't overlap with the last scheduling() request, + // we'll wait for the previous one to finish before scheduling again + scheduledPromise.then(() => { + if (isScheduling) { + console.log('ERROR: Already scheduling notifications, not scheduling again'); + } else { + scheduledPromise = scheduleNotifs(scheme, notifTimes); + //enforcing end of scheduling to conisder update through + scheduledPromise.then(() => { + resolve(); + }); + } }); + } }); - } + }); + }; /* 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 = () => { - // 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, - }; - } + // 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, + }; + }; /* EXAMPLE VALUES - present in user profile object reminder_assignment: 'passive', @@ -219,53 +217,49 @@ angular.module('emission.splash.notifscheduler', */ scheduler.getReminderPrefs = async () => { - const user = await getUser(); - 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 - } + const user = await getUser(); + 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 updateUser(newPrefs) - const updatePromise = new Promise((resolve, reject) => { - //enforcing update before moving on - update().then(() => { - resolve(); - }); + await updateUser(newPrefs); + const updatePromise = new Promise((resolve, reject) => { + //enforcing update before moving on + update().then(() => { + resolve(); }); + }); - // record the new prefs in client stats - scheduler.getReminderPrefs().then((prefs) => { - // extract only the relevant fields from the prefs, - // and add as a reading to client stats - const { reminder_assignment, - reminder_join_date, - reminder_time_of_day} = prefs; - addStatReading(statKeys.REMINDER_PREFS, { - reminder_assignment, - reminder_join_date, - reminder_time_of_day - }).then(Logger.log("Added reminder prefs to client stats")); - }); + // 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; + addStatReading(statKeys.REMINDER_PREFS, { + reminder_assignment, + reminder_join_date, + reminder_time_of_day, + }).then(Logger.log('Added reminder prefs to client stats')); + }); - return updatePromise; - } + return updatePromise; + }; $ionicPlatform.ready().then(async () => { - _config = await getConfig(); - if (!_config.reminderSchemes) { - Logger.log("No reminder schemes found in config, not scheduling notifications"); - return; - } - //setUpActions(); - update(); + _config = await getConfig(); + if (!_config.reminderSchemes) { + Logger.log('No reminder schemes found in config, not scheduling notifications'); + return; + } + //setUpActions(); + update(); }); return scheduler; -}); + }); diff --git a/www/js/splash/pushnotify.js b/www/js/splash/pushnotify.js index 775ddc4bd..28e37aaa1 100644 --- a/www/js/splash/pushnotify.js +++ b/www/js/splash/pushnotify.js @@ -1,7 +1,6 @@ //naming of this file can be a little confusing - "pushnotifysettings" for rewritten file //https://github.com/e-mission/e-mission-phone/pull/1072#discussion_r1375360832 - /* * This module deals with the interaction with the push plugin, the redirection * of silent push notifications and the re-parsing of iOS pushes. It then @@ -21,160 +20,174 @@ import angular from 'angular'; import { updateUser } from '../commHelper'; import { readConsentState, isConsented } from './startprefs'; -angular.module('emission.splash.pushnotify', ['emission.plugin.logger', - 'emission.services']) -.factory('PushNotify', function($window, $state, $rootScope, $ionicPlatform, - $ionicPopup, Logger) { - - var pushnotify = {}; - var push = null; - pushnotify.CLOUD_NOTIFICATION_EVENT = 'cloud:push:notification'; +angular + .module('emission.splash.pushnotify', ['emission.plugin.logger', 'emission.services']) + .factory( + 'PushNotify', + function ($window, $state, $rootScope, $ionicPlatform, $ionicPopup, Logger) { + 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')) { + 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) { - 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) { + 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); - }) - } 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.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', + }); + } }); - } + }; - 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; - readConsentState() - .then(isConsented) - .then(function(consentState) { - if (consentState == true) { + $ionicPlatform.ready().then(function () { + pushnotify.datacollect = $window.cordova.plugins.BEMDataCollection; + readConsentState() + .then(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'); + }); - return pushnotify; -}); + return pushnotify; + }, + ); diff --git a/www/js/splash/referral.js b/www/js/splash/referral.js index 849de847a..334fd0ebe 100644 --- a/www/js/splash/referral.js +++ b/www/js/splash/referral.js @@ -1,9 +1,10 @@ import angular from 'angular'; import { storageGetDirect, storageRemove, storageSet } from '../plugin/storage'; -angular.module('emission.splash.referral', []) +angular + .module('emission.splash.referral', []) -.factory('ReferralHandler', function($window) { + .factory('ReferralHandler', function ($window) { var referralHandler = {}; var REFERRAL_NAVIGATION_KEY = 'referral_navigation'; @@ -11,34 +12,33 @@ angular.module('emission.splash.referral', []) var REFERRED_GROUP_ID = 'referred_group_id'; var REFERRED_USER_ID = 'referred_user_id'; - referralHandler.getReferralNavigation = function() { + referralHandler.getReferralNavigation = function () { const toReturn = storageGetDirect(REFERRAL_NAVIGATION_KEY); storageRemove(REFERRAL_NAVIGATION_KEY); return toReturn; - } - - referralHandler.setupGroupReferral = function(kvList) { - storageSet(REFERRED_KEY, true); - storageSet(REFERRED_GROUP_ID, kvList['groupid']); - storageSet(REFERRED_USER_ID, kvList['userid']); - storageSet(REFERRAL_NAVIGATION_KEY, 'goals'); - }; - - referralHandler.clearGroupReferral = function(kvList) { - storageRemove(REFERRED_KEY); - storageRemove(REFERRED_GROUP_ID); - storageRemove(REFERRED_USER_ID); - storageRemove(REFERRAL_NAVIGATION_KEY); - }; - - referralHandler.getReferralParams = function(kvList) { - return [storageGetDirect(REFERRED_GROUP_ID), - storageGetDirect(REFERRED_USER_ID)]; - } - - referralHandler.hasPendingRegistration = function() { - return storageGetDirect(REFERRED_KEY) - }; - - return referralHandler; -}); + }; + + referralHandler.setupGroupReferral = function (kvList) { + storageSet(REFERRED_KEY, true); + storageSet(REFERRED_GROUP_ID, kvList['groupid']); + storageSet(REFERRED_USER_ID, kvList['userid']); + storageSet(REFERRAL_NAVIGATION_KEY, 'goals'); + }; + + referralHandler.clearGroupReferral = function (kvList) { + storageRemove(REFERRED_KEY); + storageRemove(REFERRED_GROUP_ID); + storageRemove(REFERRED_USER_ID); + storageRemove(REFERRAL_NAVIGATION_KEY); + }; + + referralHandler.getReferralParams = function (kvList) { + return [storageGetDirect(REFERRED_GROUP_ID), storageGetDirect(REFERRED_USER_ID)]; + }; + + referralHandler.hasPendingRegistration = function () { + return storageGetDirect(REFERRED_KEY); + }; + + return referralHandler; + }); diff --git a/www/js/splash/remotenotify.js b/www/js/splash/remotenotify.js index f67cb9d87..a59fdf376 100644 --- a/www/js/splash/remotenotify.js +++ b/www/js/splash/remotenotify.js @@ -18,62 +18,74 @@ import angular from 'angular'; import { addStatEvent, statKeys } from '../plugin/clientStats'; -angular.module('emission.splash.remotenotify', ['emission.plugin.logger']) - -.factory('RemoteNotify', function($http, $window, $ionicPopup, $rootScope, Logger) { +angular + .module('emission.splash.remotenotify', ['emission.plugin.logger']) + .factory('RemoteNotify', function ($http, $window, $ionicPopup, $rootScope, Logger) { var remoteNotify = {}; - remoteNotify.options = "location=yes,clearcache=no,toolbar=yes,hideurlbar=yes"; + remoteNotify.options = 'location=yes,clearcache=no,toolbar=yes,hideurlbar=yes'; /* TODO: Potentially unify with the survey URL loading */ - remoteNotify.launchWebpage = function(url) { + remoteNotify.launchWebpage = function (url) { // THIS LINE FOR inAppBrowser let iab = $window.cordova.InAppBrowser.open(url, '_blank', remoteNotify.options); - } + }; - remoteNotify.launchPopup = function(title, text) { + remoteNotify.launchPopup = function (title, text) { // THIS LINE FOR inAppBrowser let alertPopup = $ionicPopup.alert({ title: title, - template: text + template: text, }); - } + }; - remoteNotify.init = function() { - $rootScope.$on('cloud:push:notification', function(event, data) { + remoteNotify.init = function () { + $rootScope.$on('cloud:push:notification', function (event, data) { addStatEvent(statKeys.NOTIFICATION_OPEN).then(() => { - console.log("Added "+statKeys.NOTIFICATION_OPEN+" event. Data = " + JSON.stringify(data)); + console.log( + 'Added ' + statKeys.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)); - } + 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), + ); } - 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; -}); + }); diff --git a/www/js/splash/startprefs.ts b/www/js/splash/startprefs.ts index 43f29c692..75282bfd3 100644 --- a/www/js/splash/startprefs.ts +++ b/www/js/splash/startprefs.ts @@ -1,7 +1,7 @@ -import { getAngularService } from "../angular-react-helper"; +import { getAngularService } from '../angular-react-helper'; import { storageGet, storageSet } from '../plugin/storage'; import { logInfo, logDebug, displayErrorMsg } from '../plugin/logger'; -import { readIntroDone } from "../onboarding/onboardingHelper"; +import { readIntroDone } from '../onboarding/onboardingHelper'; // data collection consented protocol: string, represents the date on // which the consented protocol was approved by the IRB @@ -13,59 +13,64 @@ let _curr_consented; /** * @function writes the consent document to native storage * @returns Promise to execute the write to storage -*/ + */ function writeConsentToNative() { - //note that this calls to the notification API, + //note that this calls to the notification API, //so should not be called until we have notification permissions //see https://github.com/e-mission/e-mission-docs/issues/1006 return window['cordova'].plugins.BEMDataCollection.markConsented(_req_consent); -}; +} /** * @function marks consent in native storage, local storage, and local var * @returns Promise for marking the consent in native and local storage */ export function markConsented() { - logInfo("changing consent from " + - _curr_consented + " -> " + JSON.stringify(_req_consent)); + logInfo('changing consent from ' + _curr_consented + ' -> ' + JSON.stringify(_req_consent)); // mark in native storage - return readConsentState() - .then(writeConsentToNative) - .then(function (response) { - // mark in local storage - storageSet(DATA_COLLECTION_CONSENTED_PROTOCOL, - _req_consent); - // mark in local variable as well - _curr_consented = { ..._req_consent }; - }) - //check for reconsent - .then(readIntroDone) - .then((isIntroDone) => { - if (isIntroDone) { - logDebug("reconsent scenario - marked consent after intro done - registering pushnoify and storing device settings") - const PushNotify = getAngularService("PushNotify"); - const StoreSeviceSettings = getAngularService("StoreDeviceSettings"); - PushNotify.registerPush(); - StoreSeviceSettings.storeDeviceSettings(); - } - }) - .catch((error) => { - displayErrorMsg(error, "Error while while wrting consent to storage"); - }); -}; + return ( + readConsentState() + .then(writeConsentToNative) + .then(function (response) { + // mark in local storage + storageSet(DATA_COLLECTION_CONSENTED_PROTOCOL, _req_consent); + // mark in local variable as well + _curr_consented = { ..._req_consent }; + }) + //check for reconsent + .then(readIntroDone) + .then((isIntroDone) => { + if (isIntroDone) { + logDebug( + 'reconsent scenario - marked consent after intro done - registering pushnoify and storing device settings', + ); + const PushNotify = getAngularService('PushNotify'); + const StoreSeviceSettings = getAngularService('StoreDeviceSettings'); + PushNotify.registerPush(); + StoreSeviceSettings.storeDeviceSettings(); + } + }) + .catch((error) => { + displayErrorMsg(error, 'Error while while wrting consent to storage'); + }) + ); +} /** * @function checking for consent locally * @returns {boolean} if the consent is marked in the local var */ export function isConsented() { - logDebug("curr consented is" + JSON.stringify(_curr_consented)); - if (_curr_consented == null || _curr_consented == "" || - _curr_consented.approval_date != _req_consent.approval_date) { - logDebug("Not consented in local storage, need to show consent"); + logDebug('curr consented is' + JSON.stringify(_curr_consented)); + if ( + _curr_consented == null || + _curr_consented == '' || + _curr_consented.approval_date != _req_consent.approval_date + ) { + logDebug('Not consented in local storage, need to show consent'); return false; } else { - logDebug("Consented in local storage, no need to show consent"); + logDebug('Consented in local storage, no need to show consent'); return true; } } @@ -75,16 +80,21 @@ export function isConsented() { * @returns nothing, just reads into local variables */ export function readConsentState() { - return fetch("json/startupConfig.json") - .then(response => response.json()) + return fetch('json/startupConfig.json') + .then((response) => response.json()) .then(function (startupConfigResult) { console.log(startupConfigResult); _req_consent = startupConfigResult.emSensorDataCollectionProtocol; - logDebug("required consent version = " + JSON.stringify(_req_consent)); + logDebug('required consent version = ' + JSON.stringify(_req_consent)); return storageGet(DATA_COLLECTION_CONSENTED_PROTOCOL); - }).then(function (kv_store_consent) { + }) + .then(function (kv_store_consent) { _curr_consented = kv_store_consent; - console.assert(((_req_consent != undefined) && (_req_consent != null)), "in readConsentState $rootScope.req_consent", JSON.stringify(_req_consent)); + console.assert( + _req_consent != undefined && _req_consent != null, + 'in readConsentState $rootScope.req_consent', + JSON.stringify(_req_consent), + ); // we can just launch this, we don't need to wait for it checkNativeConsent(); }); @@ -96,15 +106,16 @@ export function readConsentState() { */ //used in ProfileSettings export function getConsentDocument() { - return window['cordova'].plugins.BEMUserCache.getDocument("config/consent", false) - .then(function (resultDoc) { + return window['cordova'].plugins.BEMUserCache.getDocument('config/consent', false).then( + function (resultDoc) { if (window['cordova'].plugins.BEMUserCache.isEmptyDoc(resultDoc)) { return null; } else { return resultDoc; } - }); -}; + }, + ); +} /** * @function checks the consent doc in native storage @@ -114,11 +125,11 @@ function checkNativeConsent() { getConsentDocument().then(function (resultDoc) { if (resultDoc == null) { if (isConsented()) { - logDebug("Local consent found, native consent missing, writing consent to native"); - displayErrorMsg("Local consent found, native consent missing, writing consent to native"); + logDebug('Local consent found, native consent missing, writing consent to native'); + displayErrorMsg('Local consent found, native consent missing, writing consent to native'); return writeConsentToNative(); } else { - logDebug("Both local and native consent not found, nothing to sync"); + logDebug('Both local and native consent not found, nothing to sync'); } } }); diff --git a/www/js/splash/storedevicesettings.js b/www/js/splash/storedevicesettings.js index 31543bc6c..ab28cde2c 100644 --- a/www/js/splash/storedevicesettings.js +++ b/www/js/splash/storedevicesettings.js @@ -1,48 +1,53 @@ import angular from 'angular'; import { updateUser } from '../commHelper'; -import { isConsented, readConsentState } from "./startprefs"; +import { isConsented, readConsentState } from './startprefs'; -angular.module('emission.splash.storedevicesettings', ['emission.plugin.logger', - 'emission.services']) -.factory('StoreDeviceSettings', function($window, $state, $rootScope, $ionicPlatform, - $ionicPopup, Logger ) { +angular + .module('emission.splash.storedevicesettings', ['emission.plugin.logger', 'emission.services']) + .factory( + 'StoreDeviceSettings', + function ($window, $state, $rootScope, $ionicPlatform, $ionicPopup, Logger) { + var storedevicesettings = {}; - var storedevicesettings = {}; + 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 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); + }); + }; - 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 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; - readConsentState() - .then(isConsented) - .then(function(consentState) { - if (consentState == true) { + $ionicPlatform.ready().then(function () { + storedevicesettings.datacollect = $window.cordova.plugins.BEMDataCollection; + readConsentState() + .then(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"); - }); + } else { + Logger.log('no consent yet, waiting to store device settings in profile'); + } + }); + Logger.log('storedevicesettings startup done'); + }); - return storedevicesettings; -}); + return storedevicesettings; + }, + ); diff --git a/www/js/survey/enketo/AddNoteButton.tsx b/www/js/survey/enketo/AddNoteButton.tsx index 1b85c728e..fb35951ee 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,20 +39,19 @@ 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; @@ -80,11 +79,14 @@ 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); @@ -94,19 +96,20 @@ 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/AddedNotesList.tsx b/www/js/survey/enketo/AddedNotesList.tsx index e29278cca..f1563c4a9 100644 --- a/www/js/survey/enketo/AddedNotesList.tsx +++ b/www/js/survey/enketo/AddedNotesList.tsx @@ -2,22 +2,21 @@ Notes are added from the AddNoteButton and are derived from survey responses. */ -import React, { useContext, useState } from "react"; -import moment from "moment"; -import { Modal } from "react-native" -import { Text, Button, DataTable, Dialog } from "react-native-paper"; -import { LabelTabContext } from "../../diary/LabelTab"; -import { getFormattedDateAbbr, isMultiDay } from "../../diary/diaryHelper"; -import { Icon } from "../../components/Icon"; -import EnketoModal from "./EnketoModal"; -import { useTranslation } from "react-i18next"; +import React, { useContext, useState } from 'react'; +import moment from 'moment'; +import { Modal } from 'react-native'; +import { Text, Button, DataTable, Dialog } from 'react-native-paper'; +import { LabelTabContext } from '../../diary/LabelTab'; +import { getFormattedDateAbbr, isMultiDay } from '../../diary/diaryHelper'; +import { Icon } from '../../components/Icon'; +import EnketoModal from './EnketoModal'; +import { useTranslation } from 'react-i18next'; type Props = { - timelineEntry: any, - additionEntries: any[], -} + timelineEntry: any; + additionEntries: any[]; +}; const AddedNotesList = ({ timelineEntry, additionEntries }: Props) => { - const { t } = useTranslation(); const { repopulateTimelineEntry } = useContext(LabelTabContext); const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] = useState(false); @@ -25,41 +24,46 @@ const AddedNotesList = ({ timelineEntry, additionEntries }: Props) => { const [editingEntry, setEditingEntry] = useState(null); function setDisplayDt(entry) { - 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 beginTs = entry.data.start_ts || entry.data.enter_ts; const stopTs = entry.data.end_ts || entry.data.exit_ts; let d; if (isMultiDay(beginTs, stopTs)) { - const beginTsZoned = moment.parseZone(beginTs*1000).tz(timezone); - const stopTsZoned = moment.parseZone(stopTs*1000).tz(timezone); + const beginTsZoned = moment.parseZone(beginTs * 1000).tz(timezone); + const stopTsZoned = moment.parseZone(stopTs * 1000).tz(timezone); d = getFormattedDateAbbr(beginTsZoned.toISOString(), stopTsZoned.toISOString()); } - const begin = moment.parseZone(beginTs*1000).tz(timezone).format('LT'); - const stop = moment.parseZone(stopTs*1000).tz(timezone).format('LT'); - return entry.displayDt = { + const begin = moment + .parseZone(beginTs * 1000) + .tz(timezone) + .format('LT'); + const stop = moment + .parseZone(stopTs * 1000) + .tz(timezone) + .format('LT'); + return (entry.displayDt = { date: d, - time: begin + " - " + stop - } + time: begin + ' - ' + stop, + }); } function deleteEntry(entry) { - console.log("Deleting entry", entry); + console.log('Deleting entry', entry); const dataKey = entry.key || entry.metadata.key; const data = entry.data; const index = additionEntries.indexOf(entry); data.status = 'DELETED'; - return window['cordova'].plugins.BEMUserCache - .putMessage(dataKey, data) - .then(() => { - additionEntries.splice(index, 1); - setConfirmDeleteModalVisible(false); - setEditingEntry(null); - }); + return window['cordova'].plugins.BEMUserCache.putMessage(dataKey, data).then(() => { + additionEntries.splice(index, 1); + setConfirmDeleteModalVisible(false); + setEditingEntry(null); + }); } function confirmDeleteEntry(entry) { @@ -90,66 +94,80 @@ const AddedNotesList = ({ timelineEntry, additionEntries }: Props) => { } const sortedEntries = additionEntries?.sort((a, b) => a.data.start_ts - b.data.start_ts); - return (<> - - {sortedEntries?.map((entry, index) => { - const isLastRow = (index == additionEntries.length - 1); - return ( - - editEntry(entry)} - style={[styles.cell, {flex: 5, pointerEvents: 'auto'}]} - textStyle={{fontSize: 12, fontWeight: 'bold'}}> - {entry.data.label} - - editEntry(entry)} - style={[styles.cell, {flex: 4}]} - textStyle={{fontSize: 12, lineHeight: 12}}> - {entry.displayDt?.date} - {entry.displayDt?.time || setDisplayDt(entry)} - - confirmDeleteEntry(entry)} - style={[styles.cell, {flex: 1}]}> - - - - ) - })} - - - - - { t('diary.delete-entry-confirm') } - - {editingEntry?.data?.label} - {editingEntry?.displayDt?.date} - {editingEntry?.displayDt?.time} - - - - - - - - ); + return ( + <> + + {sortedEntries?.map((entry, index) => { + const isLastRow = index == additionEntries.length - 1; + return ( + + editEntry(entry)} + style={[styles.cell, { flex: 5, pointerEvents: 'auto' }]} + textStyle={{ fontSize: 12, fontWeight: 'bold' }}> + {entry.data.label} + + editEntry(entry)} + style={[styles.cell, { flex: 4 }]} + textStyle={{ fontSize: 12, lineHeight: 12 }}> + {entry.displayDt?.date} + + {entry.displayDt?.time || setDisplayDt(entry)} + + + confirmDeleteEntry(entry)} + style={[styles.cell, { flex: 1 }]}> + + + + ); + })} + + + + + {t('diary.delete-entry-confirm')} + + {editingEntry?.data?.label} + {editingEntry?.displayDt?.date} + {editingEntry?.displayDt?.time} + + + + + + + + + ); }; -const styles:any = { +const styles: any = { row: (isLastRow) => ({ minHeight: 36, height: 36, - borderBottomWidth: (isLastRow ? 0 : 1), + borderBottomWidth: isLastRow ? 0 : 1, borderBottomColor: 'rgba(0,0,0,0.1)', pointerEvents: 'all', }), cell: { pointerEvents: 'auto', }, -} +}; export default AddedNotesList; diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index 8b80b6dfe..de1f505f3 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -10,13 +10,12 @@ import { displayError, displayErrorMsg } from '../../plugin/logger'; // import { transform } from 'enketo-transformer/web'; type Props = Omit & { - surveyName: string, - onResponseSaved: (response: any) => void, - opts?: SurveyOptions, -} - -const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => { + surveyName: string; + onResponseSaved: (response: any) => void; + opts?: SurveyOptions; +}; +const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const { t, i18n } = useTranslation(); const headerEl = useRef(null); const surveyJson = useRef(null); @@ -27,9 +26,11 @@ 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(); @@ -41,18 +42,21 @@ 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; @@ -89,14 +93,18 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => Just make sure to keep a .form-language-selector element into which the form language selector ( @@ -111,16 +119,44 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) =>
{/* Used some quick-and-dirty inline CSS styles here because the form-footer should be styled in the mother application. The HTML markup can be changed as well. */} - {t('survey.back')} - - {t('survey.next')} -
{t('survey.powered-by')} enketo logo
+ + {t('survey.next')} + +
+ {t('survey.powered-by')}{' '} + + enketo logo + {' '} +
{/*
    */}
    @@ -129,19 +165,17 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => ); return ( - - - - -
    - {enketoContent} -
    + + + + +
    {enketoContent}
    ); -} +}; const s = StyleSheet.create({ dismissBtn: { @@ -152,7 +186,7 @@ const s = StyleSheet.create({ display: 'flex', alignItems: 'center', padding: 0, - } + }, }); export default EnketoModal; diff --git a/www/js/survey/enketo/UserInputButton.tsx b/www/js/survey/enketo/UserInputButton.tsx index 68d0ae944..fa2412b73 100644 --- a/www/js/survey/enketo/UserInputButton.tsx +++ b/www/js/survey/enketo/UserInputButton.tsx @@ -8,18 +8,18 @@ The start and end times of the addition are the same as the trip or place. */ -import React, { useContext, useMemo, useState } from "react"; -import { getAngularService } from "../../angular-react-helper"; -import DiaryButton from "../../components/DiaryButton"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "react-native-paper"; -import { displayErrorMsg, logDebug } from "../../plugin/logger"; -import EnketoModal from "./EnketoModal"; -import { LabelTabContext } from "../../diary/LabelTab"; +import React, { useContext, useMemo, useState } from 'react'; +import { getAngularService } from '../../angular-react-helper'; +import DiaryButton from '../../components/DiaryButton'; +import { useTranslation } from 'react-i18next'; +import { useTheme } from 'react-native-paper'; +import { displayErrorMsg, logDebug } from '../../plugin/logger'; +import EnketoModal from './EnketoModal'; +import { LabelTabContext } from '../../diary/LabelTab'; type Props = { - timelineEntry: any, -} + timelineEntry: any; +}; const UserInputButton = ({ timelineEntry }: Props) => { const { colors } = useTheme(); const { t, i18n } = useTranslation(); @@ -28,13 +28,14 @@ const UserInputButton = ({ timelineEntry }: Props) => { const [modalVisible, setModalVisible] = useState(false); const { repopulateTimelineEntry } = useContext(LabelTabContext); - const EnketoTripButtonService = getAngularService("EnketoTripButtonService"); + const EnketoTripButtonService = getAngularService('EnketoTripButtonService'); const etbsSingleKey = EnketoTripButtonService.SINGLE_KEY; // the label resolved from the survey response, or null if there is no response yet - const responseLabel = useMemo(() => ( - timelineEntry.userInput?.[etbsSingleKey]?.data?.label || null - ), [timelineEntry]); + const responseLabel = useMemo( + () => timelineEntry.userInput?.[etbsSingleKey]?.data?.label || null, + [timelineEntry], + ); function launchUserInputSurvey() { logDebug('UserInputButton: About to launch survey'); @@ -45,31 +46,37 @@ const UserInputButton = ({ timelineEntry }: Props) => { function onResponseSaved(result) { if (result) { - logDebug('UserInputButton: response was saved, about to repopulateTimelineEntry; result=' + JSON.stringify(result)); + logDebug( + 'UserInputButton: response was saved, about to repopulateTimelineEntry; result=' + + JSON.stringify(result), + ); repopulateTimelineEntry(timelineEntry._id.$oid); } else { displayErrorMsg('UserInputButton: response was not saved, result=', result); } } - return (<> - launchUserInputSurvey()}> - {/* if no response yet, show the default label */} - {responseLabel || t('diary.choose-survey')} - + return ( + <> + launchUserInputSurvey()}> + {/* if no response yet, show the default label */} + {responseLabel || t('diary.choose-survey')} + - setModalVisible(false)} - onResponseSaved={onResponseSaved} - surveyName={'TripConfirmSurvey'} /* As of now, the survey name is hardcoded. + setModalVisible(false)} + onResponseSaved={onResponseSaved} + surveyName={'TripConfirmSurvey'} /* As of now, the survey name is hardcoded. In the future, if we ever implement something like a "Place Details" survey, we may want to make this configurable. */ - opts={{ timelineEntry, - prefilledSurveyResponse: prevSurveyResponse - }} /> - ); + opts={{ timelineEntry, prefilledSurveyResponse: prevSurveyResponse }} + /> + + ); }; export default UserInputButton; diff --git a/www/js/survey/enketo/answer.js b/www/js/survey/enketo/answer.js index e6077c479..cb5745037 100644 --- a/www/js/survey/enketo/answer.js +++ b/www/js/survey/enketo/answer.js @@ -2,192 +2,191 @@ import angular from 'angular'; import MessageFormat from 'messageformat'; import { getConfig } from '../../config/dynamicConfig'; -angular.module('emission.survey.enketo.answer', ['ionic']) -.factory('EnketoSurveyAnswer', function($http) { - /** - * @typedef EnketoAnswerData - * @type {object} - * @property {string} label - display label (this value is use for displaying on the button) - * @property {string} ts - the timestamp at which the survey was filled out (in seconds) - * @property {string} fmt_time - the formatted timestamp at which the survey was filled out - * @property {string} name - survey name - * @property {string} version - survey version - * @property {string} xmlResponse - survey answer XML string - * @property {string} jsonDocResponse - survey answer JSON object - */ - - /** - * @typedef EnketoAnswer - * @type {object} - * @property {EnketoAnswerData} data - answer data - * @property {{[labelField:string]: string}} [labels] - virtual labels (populated by populateLabels method) - */ - - /** - * @typedef EnketoSurveyConfig - * @type {{ - * [surveyName:string]: { - * formPath: string; - * labelFields: string[]; - * version: number; - * compatibleWith: number; - * } - * }} - */ - - const LABEL_FUNCTIONS = { - UseLabelTemplate: (xmlDoc, name) => { - - return _lazyLoadConfig().then(configSurveys => { - - const config = configSurveys[name]; // config for this survey - const lang = i18next.resolvedLanguage; - const labelTemplate = config.labelTemplate?.[lang]; - - if (!labelTemplate) return "Answered"; // no template given in config - if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, - // so we return the unaltered template - - // gather vars that will be interpolated into the template according to the survey config - const labelVars = {} - for (let lblVar in config.labelVars) { - const fieldName = config.labelVars[lblVar].key; - let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); - if (fieldStr == '') fieldStr = null; - if (config.labelVars[lblVar].type == 'length') { - const fieldMatches = fieldStr?.split(' '); - labelVars[lblVar] = fieldMatches?.length || 0; - } else { - throw new Error(`labelVar type ${config.labelVars[lblVar].type } is not supported!`) +angular + .module('emission.survey.enketo.answer', ['ionic']) + .factory('EnketoSurveyAnswer', function ($http) { + /** + * @typedef EnketoAnswerData + * @type {object} + * @property {string} label - display label (this value is use for displaying on the button) + * @property {string} ts - the timestamp at which the survey was filled out (in seconds) + * @property {string} fmt_time - the formatted timestamp at which the survey was filled out + * @property {string} name - survey name + * @property {string} version - survey version + * @property {string} xmlResponse - survey answer XML string + * @property {string} jsonDocResponse - survey answer JSON object + */ + + /** + * @typedef EnketoAnswer + * @type {object} + * @property {EnketoAnswerData} data - answer data + * @property {{[labelField:string]: string}} [labels] - virtual labels (populated by populateLabels method) + */ + + /** + * @typedef EnketoSurveyConfig + * @type {{ + * [surveyName:string]: { + * formPath: string; + * labelFields: string[]; + * version: number; + * compatibleWith: number; + * } + * }} + */ + + const LABEL_FUNCTIONS = { + UseLabelTemplate: (xmlDoc, name) => { + return _lazyLoadConfig().then((configSurveys) => { + const config = configSurveys[name]; // config for this survey + const lang = i18next.resolvedLanguage; + const labelTemplate = config.labelTemplate?.[lang]; + + if (!labelTemplate) return 'Answered'; // no template given in config + if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, + // so we return the unaltered template + + // gather vars that will be interpolated into the template according to the survey config + const labelVars = {}; + for (let lblVar in config.labelVars) { + const fieldName = config.labelVars[lblVar].key; + let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); + if (fieldStr == '') fieldStr = null; + if (config.labelVars[lblVar].type == 'length') { + const fieldMatches = fieldStr?.split(' '); + labelVars[lblVar] = fieldMatches?.length || 0; + } else { + throw new Error(`labelVar type ${config.labelVars[lblVar].type} is not supported!`); + } } - } - // use MessageFormat interpolate the label template with the label vars - const mf = new MessageFormat(lang); - const label = mf.compile(labelTemplate)(labelVars); - return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas - }) + // use MessageFormat interpolate the label template with the label vars + const mf = new MessageFormat(lang); + const label = mf.compile(labelTemplate)(labelVars); + return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas + }); + }, + }; + + /** @type {EnketoSurveyConfig} _config */ + let _config; + + /** + * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. + * @param {XMLDocument} xmlDoc survey answer object + * @param {string} tagName tag name + * @returns {string} answer string. If not found, return "\" + */ + function _getAnswerByTagName(xmlDoc, tagName) { + const vals = xmlDoc.getElementsByTagName(tagName); + const val = vals.length ? vals[0].innerHTML : null; + if (!val) return ''; + return val; } - }; - - /** @type {EnketoSurveyConfig} _config */ - let _config; - - /** - * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. - * @param {XMLDocument} xmlDoc survey answer object - * @param {string} tagName tag name - * @returns {string} answer string. If not found, return "\" - */ - function _getAnswerByTagName(xmlDoc, tagName) { - const vals = xmlDoc.getElementsByTagName(tagName); - const val = vals.length ? vals[0].innerHTML : null; - if (!val) return ''; - return val; - } - - /** - * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config - * @returns {Promise} enketo survey config - */ - function _lazyLoadConfig() { - if (_config !== undefined) { - return Promise.resolve(_config); + + /** + * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config + * @returns {Promise} enketo survey config + */ + function _lazyLoadConfig() { + if (_config !== undefined) { + return Promise.resolve(_config); + } + return getConfig().then((newConfig) => { + Logger.log('Resolved UI_CONFIG_READY promise in answer.js, filling in templates'); + _config = newConfig.survey_info.surveys; + return _config; + }); + } + + /** + * filterByNameAndVersion filter the survey answers by survey name and their version. + * The version for filtering is specified in enketo survey `compatibleWith` config. + * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. + * @param {string} name survey name (defined in enketo survey config) + * @param {EnketoAnswer[]} answers survey answers + * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. + * @return {Promise} filtered survey answers + */ + function filterByNameAndVersion(name, answers) { + return _lazyLoadConfig().then((config) => + answers.filter( + (answer) => + answer.data.name === name && answer.data.version >= config[name].compatibleWith, + ), + ); } - return getConfig().then((newConfig) => { - Logger.log("Resolved UI_CONFIG_READY promise in answer.js, filling in templates"); - _config = newConfig.survey_info.surveys; - return _config; - }) - } - - /** - * filterByNameAndVersion filter the survey answers by survey name and their version. - * The version for filtering is specified in enketo survey `compatibleWith` config. - * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. - * @param {string} name survey name (defined in enketo survey config) - * @param {EnketoAnswer[]} answers survey answers - * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. - * @return {Promise} filtered survey answers - */ - function filterByNameAndVersion(name, answers) { - return _lazyLoadConfig().then(config => - answers.filter(answer => - answer.data.name === name && - answer.data.version >= config[name].compatibleWith - ) - ); - } - - /** - * resolve answer label for the survey - * @param {string} name survey name - * @param {XMLDocument} xmlDoc survey answer object - * @returns {Promise} label string Promise - */ - function resolveLabel(name, xmlDoc) { - // Some studies may want a custom label function for their survey. - // Those can be added in LABEL_FUNCTIONS with the survey name as the key. - // Otherwise, UseLabelTemplate will create a label using the template in the config - if (LABEL_FUNCTIONS[name]) - return LABEL_FUNCTIONS[name](xmlDoc); - return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); - } - - /** - * resolve timestamps label from the survey response - * @param {XMLDocument} xmlDoc survey answer object - * @param {object} trip trip object - * @returns {object} object with `start_ts` and `end_ts` - * - null if no timestamps are resolved - * - undefined if the timestamps are invalid - */ - function resolveTimestamps(xmlDoc, timelineEntry) { - // check for Date and Time fields - const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; - let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; - const endDate = xmlDoc.getElementsByTagName('End_date')?.[0]?.innerHTML; - let endTime = xmlDoc.getElementsByTagName('End_time')?.[0]?.innerHTML; - - // if any of the fields are missing, return null - if (!startDate || !startTime || !endDate || !endTime) return null; - - const timezone = timelineEntry.start_local_dt?.timezone - || timelineEntry.enter_local_dt?.timezone - || timelineEntry.end_local_dt?.timezone - || timelineEntry.exit_local_dt?.timezone; - // split by + or - to get time without offset - startTime = startTime.split(/\-|\+/)[0]; - endTime = endTime.split(/\-|\+/)[0]; - - let additionStartTs = moment.tz(startDate+'T'+startTime, timezone).unix(); - let additionEndTs = moment.tz(endDate+'T'+endTime, timezone).unix(); - - if (additionStartTs > additionEndTs) { - return undefined; // if the start time is after the end time, this is an invalid response + + /** + * resolve answer label for the survey + * @param {string} name survey name + * @param {XMLDocument} xmlDoc survey answer object + * @returns {Promise} label string Promise + */ + function resolveLabel(name, xmlDoc) { + // Some studies may want a custom label function for their survey. + // Those can be added in LABEL_FUNCTIONS with the survey name as the key. + // Otherwise, UseLabelTemplate will create a label using the template in the config + if (LABEL_FUNCTIONS[name]) return LABEL_FUNCTIONS[name](xmlDoc); + return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); } - /* Enketo survey time inputs are only precise to the minute, while trips/places are precise to + /** + * resolve timestamps label from the survey response + * @param {XMLDocument} xmlDoc survey answer object + * @param {object} trip trip object + * @returns {object} object with `start_ts` and `end_ts` + * - null if no timestamps are resolved + * - undefined if the timestamps are invalid + */ + function resolveTimestamps(xmlDoc, timelineEntry) { + // check for Date and Time fields + const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; + let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; + const endDate = xmlDoc.getElementsByTagName('End_date')?.[0]?.innerHTML; + let endTime = xmlDoc.getElementsByTagName('End_time')?.[0]?.innerHTML; + + // if any of the fields are missing, return null + if (!startDate || !startTime || !endDate || !endTime) return null; + + const timezone = + timelineEntry.start_local_dt?.timezone || + timelineEntry.enter_local_dt?.timezone || + timelineEntry.end_local_dt?.timezone || + timelineEntry.exit_local_dt?.timezone; + // split by + or - to get time without offset + startTime = startTime.split(/\-|\+/)[0]; + endTime = endTime.split(/\-|\+/)[0]; + + let additionStartTs = moment.tz(startDate + 'T' + startTime, timezone).unix(); + let additionEndTs = moment.tz(endDate + 'T' + endTime, timezone).unix(); + + if (additionStartTs > additionEndTs) { + return undefined; // if the start time is after the end time, this is an invalid response + } + + /* Enketo survey time inputs are only precise to the minute, while trips/places are precise to the millisecond. To avoid precision issues, we will check if the start/end timestamps from the survey response are within the same minute as the start/end or enter/exit timestamps. If so, we will use the exact trip/place timestamps */ - const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; - const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; - if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) - additionStartTs = entryStartTs; - if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) - additionEndTs = entryEndTs; - - // return unix timestamps in seconds + const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; + const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; + if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) + additionStartTs = entryStartTs; + if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) + additionEndTs = entryEndTs; + + // return unix timestamps in seconds + return { + start_ts: additionStartTs, + end_ts: additionEndTs, + }; + } + return { - start_ts: additionStartTs, - end_ts: additionEndTs - }; - } - - return { - filterByNameAndVersion, - resolveLabel, - resolveTimestamps, - }; -}); + filterByNameAndVersion, + resolveLabel, + resolveTimestamps, + }; + }); diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index 49f7747f6..a5bb7edd2 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -4,102 +4,130 @@ import angular from 'angular'; -angular.module('emission.survey.enketo.add-note-button', - ['emission.services', - 'emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoNotesButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { - var enbs = {}; - console.log("Creating EnketoNotesButtonService"); - enbs.SINGLE_KEY="NOTES"; - enbs.MANUAL_KEYS = []; +angular + .module('emission.survey.enketo.add-note-button', [ + 'emission.services', + 'emission.survey.enketo.answer', + 'emission.survey.inputmatcher', + ]) + .factory( + 'EnketoNotesButtonService', + function (InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + var enbs = {}; + console.log('Creating EnketoNotesButtonService'); + enbs.SINGLE_KEY = 'NOTES'; + enbs.MANUAL_KEYS = []; - /** - * Set the keys for trip and/or place additions whichever will be enabled, - * and sets the name of the surveys they will use. - */ - enbs.initConfig = function(tripSurveyName, placeSurveyName) { - enbs.tripSurveyName = tripSurveyName; - if (tripSurveyName) { - enbs.MANUAL_KEYS.push("manual/trip_addition_input") - } - enbs.placeSurveyName = placeSurveyName; - if (placeSurveyName) { - enbs.MANUAL_KEYS.push("manual/place_addition_input") - } - } + /** + * Set the keys for trip and/or place additions whichever will be enabled, + * and sets the name of the surveys they will use. + */ + enbs.initConfig = function (tripSurveyName, placeSurveyName) { + enbs.tripSurveyName = tripSurveyName; + if (tripSurveyName) { + enbs.MANUAL_KEYS.push('manual/trip_addition_input'); + } + enbs.placeSurveyName = placeSurveyName; + if (placeSurveyName) { + enbs.MANUAL_KEYS.push('manual/place_addition_input'); + } + }; - /** - * Embed 'inputType' to the timelineEntry. - */ - enbs.extractResult = function(results) { - const resultsPromises = [EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results)]; - if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { - resultsPromises.push(EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results)); - } - return Promise.all(resultsPromises); - }; + /** + * Embed 'inputType' to the timelineEntry. + */ + enbs.extractResult = function (results) { + const resultsPromises = [ + EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results), + ]; + if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { + resultsPromises.push( + EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results), + ); + } + return Promise.all(resultsPromises); + }; - enbs.processManualInputs = function(manualResults, resultMap) { - console.log("ENKETO: processManualInputs with ", manualResults, " and ", resultMap); - const surveyResults = manualResults.flat(2); - resultMap[enbs.SINGLE_KEY] = surveyResults; - } + enbs.processManualInputs = function (manualResults, resultMap) { + console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); + const surveyResults = manualResults.flat(2); + resultMap[enbs.SINGLE_KEY] = surveyResults; + }; - enbs.populateInputsAndInferences = function(timelineEntry, manualResultMap) { - console.log("ENKETO: populating timelineEntry,", timelineEntry, " with result map", manualResultMap); - if (angular.isDefined(timelineEntry)) { - // initialize additions array as empty if it doesn't already exist - timelineEntry.additionsList ||= []; - enbs.populateManualInputs(timelineEntry, enbs.SINGLE_KEY, manualResultMap[enbs.SINGLE_KEY]); - } else { - console.log("timelineEntry information not yet bound, skipping fill"); - } - } + enbs.populateInputsAndInferences = function (timelineEntry, manualResultMap) { + console.log( + 'ENKETO: populating timelineEntry,', + timelineEntry, + ' with result map', + manualResultMap, + ); + if (angular.isDefined(timelineEntry)) { + // initialize additions array as empty if it doesn't already exist + timelineEntry.additionsList ||= []; + enbs.populateManualInputs( + timelineEntry, + enbs.SINGLE_KEY, + manualResultMap[enbs.SINGLE_KEY], + ); + } else { + console.log('timelineEntry information not yet bound, skipping fill'); + } + }; - /** - * Embed 'inputType' to the timelineEntry - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - enbs.populateManualInputs = function (timelineEntry, inputType, inputList) { - // there is not necessarily just one addition per timeline entry, - // so unlike user inputs, we don't want to replace the server entry with - // the unprocessed entry - // but we also don't want to blindly append the unprocessed entry; what - // if it was a deletion. - // what we really want to do is to merge the unprocessed and processed entries - // taking deletion into account - // one option for that is to just combine the processed and unprocessed entries - // into a single list - // note that this is not necessarily the most performant approach, since we will - // be re-matching entries that have already been matched on the server - // but the number of matched entries is likely to be small, so we can live - // with the performance for now - const unprocessedAdditions = InputMatcher.getAdditionsForTimelineEntry(timelineEntry, inputList); - const combinedPotentialAdditionList = timelineEntry.additions.concat(unprocessedAdditions); - const dedupedList = InputMatcher.getUniqueEntries(combinedPotentialAdditionList); - Logger.log("After combining unprocessed ("+unprocessedAdditions.length+ - ") with server ("+timelineEntry.additions.length+ - ") for a combined ("+combinedPotentialAdditionList.length+ - "), deduped entries are ("+dedupedList.length+")"); + /** + * Embed 'inputType' to the timelineEntry + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + enbs.populateManualInputs = function (timelineEntry, inputType, inputList) { + // there is not necessarily just one addition per timeline entry, + // so unlike user inputs, we don't want to replace the server entry with + // the unprocessed entry + // but we also don't want to blindly append the unprocessed entry; what + // if it was a deletion. + // what we really want to do is to merge the unprocessed and processed entries + // taking deletion into account + // one option for that is to just combine the processed and unprocessed entries + // into a single list + // note that this is not necessarily the most performant approach, since we will + // be re-matching entries that have already been matched on the server + // but the number of matched entries is likely to be small, so we can live + // with the performance for now + const unprocessedAdditions = InputMatcher.getAdditionsForTimelineEntry( + timelineEntry, + inputList, + ); + const combinedPotentialAdditionList = timelineEntry.additions.concat(unprocessedAdditions); + const dedupedList = InputMatcher.getUniqueEntries(combinedPotentialAdditionList); + Logger.log( + 'After combining unprocessed (' + + unprocessedAdditions.length + + ') with server (' + + timelineEntry.additions.length + + ') for a combined (' + + combinedPotentialAdditionList.length + + '), deduped entries are (' + + dedupedList.length + + ')', + ); - enbs.populateInput(timelineEntry.additionsList, inputType, dedupedList); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - enbs.editingTrip = angular.undefined; - } + enbs.populateInput(timelineEntry.additionsList, inputType, dedupedList); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + enbs.editingTrip = angular.undefined; + }; - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - enbs.populateInput = function(timelineEntryField, inputType, userInputEntry) { - if (angular.isDefined(userInputEntry)) { - timelineEntryField.length = 0; - userInputEntry.forEach(ta => { + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + enbs.populateInput = function (timelineEntryField, inputType, userInputEntry) { + if (angular.isDefined(userInputEntry)) { + timelineEntryField.length = 0; + userInputEntry.forEach((ta) => { timelineEntryField.push(ta); }); - } - } + } + }; - return enbs; -}); + return enbs; + }, + ); diff --git a/www/js/survey/enketo/enketo-trip-button.js b/www/js/survey/enketo/enketo-trip-button.js index 6e710435f..66cf82cd7 100644 --- a/www/js/survey/enketo/enketo-trip-button.js +++ b/www/js/survey/enketo/enketo-trip-button.js @@ -13,99 +13,111 @@ import angular from 'angular'; -angular.module('emission.survey.enketo.trip.button', - ['emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoTripButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { - var etbs = {}; - console.log("Creating EnketoTripButtonService"); - etbs.key = "manual/trip_user_input"; - etbs.SINGLE_KEY="SURVEY"; - etbs.MANUAL_KEYS = [etbs.key]; +angular + .module('emission.survey.enketo.trip.button', [ + 'emission.survey.enketo.answer', + 'emission.survey.inputmatcher', + ]) + .factory( + 'EnketoTripButtonService', + function (InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + var etbs = {}; + console.log('Creating EnketoTripButtonService'); + etbs.key = 'manual/trip_user_input'; + etbs.SINGLE_KEY = 'SURVEY'; + etbs.MANUAL_KEYS = [etbs.key]; - /** - * Embed 'inputType' to the trip. - */ - etbs.extractResult = (results) => EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); + /** + * Embed 'inputType' to the trip. + */ + etbs.extractResult = (results) => + EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); - etbs.processManualInputs = function(manualResults, resultMap) { - if (manualResults.length > 1) { - Logger.displayError("Found "+manualResults.length+" results expected 1", manualResults); - } else { - console.log("ENKETO: processManualInputs with ", manualResults, " and ", resultMap); - const surveyResult = manualResults[0]; - resultMap[etbs.SINGLE_KEY] = surveyResult; - } - } + etbs.processManualInputs = function (manualResults, resultMap) { + if (manualResults.length > 1) { + Logger.displayError( + 'Found ' + manualResults.length + ' results expected 1', + manualResults, + ); + } else { + console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); + const surveyResult = manualResults[0]; + resultMap[etbs.SINGLE_KEY] = surveyResult; + } + }; - etbs.populateInputsAndInferences = function(trip, manualResultMap) { - console.log("ENKETO: populating trip,", trip, " with result map", manualResultMap); - if (angular.isDefined(trip)) { - // console.log("Expectation: "+JSON.stringify(trip.expectation)); - // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); - trip.userInput = {}; - etbs.populateManualInputs(trip, trip.getNextEntry(), etbs.SINGLE_KEY, - manualResultMap[etbs.SINGLE_KEY]); - trip.finalInference = {}; - etbs.inferFinalLabels(trip); - etbs.updateVerifiability(trip); - } else { - console.log("Trip information not yet bound, skipping fill"); - } - } + etbs.populateInputsAndInferences = function (trip, manualResultMap) { + console.log('ENKETO: populating trip,', trip, ' with result map', manualResultMap); + if (angular.isDefined(trip)) { + // console.log("Expectation: "+JSON.stringify(trip.expectation)); + // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); + trip.userInput = {}; + etbs.populateManualInputs( + trip, + trip.getNextEntry(), + etbs.SINGLE_KEY, + manualResultMap[etbs.SINGLE_KEY], + ); + trip.finalInference = {}; + etbs.inferFinalLabels(trip); + etbs.updateVerifiability(trip); + } else { + console.log('Trip information not yet bound, skipping fill'); + } + }; - /** - * Embed 'inputType' to the trip - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { - // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, - inputList); - var userInputEntry = unprocessedLabelEntry; - if (!angular.isDefined(userInputEntry)) { + /** + * Embed 'inputType' to the trip + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { + // Check unprocessed labels first since they are more recent + const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, inputList); + var userInputEntry = unprocessedLabelEntry; + if (!angular.isDefined(userInputEntry)) { userInputEntry = trip.user_input?.[etbs.inputType2retKey(inputType)]; - } - etbs.populateInput(trip.userInput, inputType, userInputEntry); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - etbs.editingTrip = angular.undefined; - } + } + etbs.populateInput(trip.userInput, inputType, userInputEntry); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + etbs.editingTrip = angular.undefined; + }; - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - etbs.populateInput = function(tripField, inputType, userInputEntry) { - if (angular.isDefined(userInputEntry)) { - tripField[inputType] = userInputEntry; - } - } + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + etbs.populateInput = function (tripField, inputType, userInputEntry) { + if (angular.isDefined(userInputEntry)) { + tripField[inputType] = userInputEntry; + } + }; - /** - * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. - * The algorithm below operationalizes these principles: - * - Never consider label tuples that contradict a green label - * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before - * - After filtering, predict the most likely choices at the level of individual labels, not label tuples - * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold - */ - etbs.inferFinalLabels = function(trip) { - // currently a NOP since we don't have any other trip properties - return; - } + /** + * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. + * The algorithm below operationalizes these principles: + * - Never consider label tuples that contradict a green label + * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before + * - After filtering, predict the most likely choices at the level of individual labels, not label tuples + * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold + */ + etbs.inferFinalLabels = function (trip) { + // currently a NOP since we don't have any other trip properties + return; + }; - /** - * MODE (becomes manual/mode_confirm) becomes mode_confirm - */ - etbs.inputType2retKey = function(inputType) { - return etbs.key.split("/")[1]; - } + /** + * MODE (becomes manual/mode_confirm) becomes mode_confirm + */ + etbs.inputType2retKey = function (inputType) { + return etbs.key.split('/')[1]; + }; - etbs.updateVerifiability = function(trip) { - // currently a NOP since we don't have any other trip properties - trip.verifiability = "cannot-verify"; - return; - } + etbs.updateVerifiability = function (trip) { + // currently a NOP since we don't have any other trip properties + trip.verifiability = 'cannot-verify'; + return; + }; - return etbs; -}); + return etbs; + }, + ); diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 6e9147cf8..b1e228540 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -1,10 +1,10 @@ -import { getAngularService } from "../../angular-react-helper"; +import { getAngularService } from '../../angular-react-helper'; import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; -import { logDebug } from "../../plugin/logger"; +import { logDebug } from '../../plugin/logger'; -export type PrefillFields = {[key: string]: string}; +export type PrefillFields = { [key: string]: string }; export type SurveyOptions = { undismissable?: boolean; @@ -37,12 +37,10 @@ function getXmlWithPrefills(xmlModel: string, prefillFields: PrefillFields) { * @param opts object with options like 'prefilledSurveyResponse' or 'prefillFields' * @returns XML string of an existing or prefilled model response, or null if no response is available */ -export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|null { +export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string | null { if (!xmlModel) return null; - if (opts.prefilledSurveyResponse) - return opts.prefilledSurveyResponse; - if (opts.prefillFields) - return getXmlWithPrefills(xmlModel, opts.prefillFields); + if (opts.prefilledSurveyResponse) return opts.prefilledSurveyResponse; + if (opts.prefillFields) return getXmlWithPrefills(xmlModel, opts.prefillFields); return null; } @@ -58,58 +56,59 @@ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, op const xmlParser = new window.DOMParser(); const xmlResponse = enketoForm.getDataStr(); const xmlDoc = xmlParser.parseFromString(xmlResponse, 'text/xml'); - const xml2js = new XMLParser({ignoreAttributes: false, attributeNamePrefix: 'attr'}); + const xml2js = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: 'attr' }); const jsonDocResponse = xml2js.parse(xmlResponse); - return EnketoSurveyAnswer.resolveLabel(surveyName, xmlDoc).then(rsLabel => { - const data: any = { - label: rsLabel, - name: surveyName, - version: appConfig.survey_info.surveys[surveyName].version, - xmlResponse, - jsonDocResponse, - }; - if (opts.timelineEntry) { - let timestamps = EnketoSurveyAnswer.resolveTimestamps(xmlDoc, opts.timelineEntry); - if (timestamps === undefined) { - // timestamps were resolved, but they are invalid - return new Error(i18next.t('survey.enketo-timestamps-invalid')); //"Timestamps are invalid. Please ensure that the start time is before the end time."); + return EnketoSurveyAnswer.resolveLabel(surveyName, xmlDoc) + .then((rsLabel) => { + const data: any = { + label: rsLabel, + name: surveyName, + version: appConfig.survey_info.surveys[surveyName].version, + xmlResponse, + jsonDocResponse, + }; + if (opts.timelineEntry) { + let timestamps = EnketoSurveyAnswer.resolveTimestamps(xmlDoc, opts.timelineEntry); + if (timestamps === undefined) { + // timestamps were resolved, but they are invalid + return new Error(i18next.t('survey.enketo-timestamps-invalid')); //"Timestamps are invalid. Please ensure that the start time is before the end time."); + } + // if timestamps were not resolved from the survey, we will use the trip or place timestamps + timestamps ||= opts.timelineEntry; + data.start_ts = timestamps.start_ts || timestamps.enter_ts; + data.end_ts = timestamps.end_ts || timestamps.exit_ts; + // UUID generated using this method https://stackoverflow.com/a/66332305 + data.match_id = URL.createObjectURL(new Blob([])).slice(-36); + } else { + const now = Date.now(); + data.ts = now / 1000; // convert to seconds to be consistent with the server + data.fmt_time = new Date(now); } - // if timestamps were not resolved from the survey, we will use the trip or place timestamps - timestamps ||= opts.timelineEntry; - data.start_ts = timestamps.start_ts || timestamps.enter_ts; - data.end_ts = timestamps.end_ts || timestamps.exit_ts; - // UUID generated using this method https://stackoverflow.com/a/66332305 - data.match_id = URL.createObjectURL(new Blob([])).slice(-36); - } else { - const now = Date.now(); - data.ts = now/1000; // convert to seconds to be consistent with the server - data.fmt_time = new Date(now); - } - // use dataKey passed into opts if available, otherwise get it from the config - const dataKey = opts.dataKey || appConfig.survey_info.surveys[surveyName].dataKey; - return window['cordova'].plugins.BEMUserCache - .putMessage(dataKey, data) - .then(() => data); - }).then(data => data); + // use dataKey passed into opts if available, otherwise get it from the config + const dataKey = opts.dataKey || appConfig.survey_info.surveys[surveyName].dataKey; + return window['cordova'].plugins.BEMUserCache.putMessage(dataKey, data).then(() => data); + }) + .then((data) => data); } const _getMostRecent = (answers) => { answers.sort((a, b) => a.metadata.write_ts < b.metadata.write_ts); - console.log("first answer is ", answers[0], " last answer is ", answers[answers.length-1]); + console.log('first answer is ', answers[0], ' last answer is ', answers[answers.length - 1]); return answers[0]; -} +}; /* - * We retrieve all the records every time instead of caching because of the - * usage pattern. We assume that the demographic survey is edited fairly - * rarely, so loading it every time will likely do a bunch of unnecessary work. - * Loading it on demand seems like the way to go. If we choose to experiment - * with incremental updates, we may want to revisit this. -*/ + * We retrieve all the records every time instead of caching because of the + * usage pattern. We assume that the demographic survey is edited fairly + * rarely, so loading it every time will likely do a bunch of unnecessary work. + * Loading it on demand seems like the way to go. If we choose to experiment + * with incremental updates, we may want to revisit this. + */ export function loadPreviousResponseForSurvey(dataKey: string) { const UnifiedDataLoader = getAngularService('UnifiedDataLoader'); const tq = window['cordova'].plugins.BEMUserCache.getAllTimeQuery(); - logDebug("loadPreviousResponseForSurvey: dataKey = " + dataKey + "; tq = " + tq); - return UnifiedDataLoader.getUnifiedMessagesForInterval(dataKey, tq) - .then(answers => _getMostRecent(answers)) + logDebug('loadPreviousResponseForSurvey: dataKey = ' + dataKey + '; tq = ' + tq); + return UnifiedDataLoader.getUnifiedMessagesForInterval(dataKey, tq).then((answers) => + _getMostRecent(answers), + ); } diff --git a/www/js/survey/enketo/infinite_scroll_filters.ts b/www/js/survey/enketo/infinite_scroll_filters.ts index 98eba65db..bc43591e0 100644 --- a/www/js/survey/enketo/infinite_scroll_filters.ts +++ b/www/js/survey/enketo/infinite_scroll_filters.ts @@ -6,33 +6,29 @@ * All UI elements should only use $scope variables. */ -import i18next from "i18next"; -import { getAngularService } from "../../angular-react-helper"; +import i18next from 'i18next'; +import { getAngularService } from '../../angular-react-helper'; const unlabeledCheck = (t) => { - try { - const EnketoTripButtonService = getAngularService("EnketoTripButtonService"); - const etbsSingleKey = EnketoTripButtonService.SINGLE_KEY; - return typeof t.userInput[etbsSingleKey] === 'undefined'; - } - catch (e) { - console.log("Error in retrieving EnketoTripButtonService: ", e); - } -} + try { + const EnketoTripButtonService = getAngularService('EnketoTripButtonService'); + const etbsSingleKey = EnketoTripButtonService.SINGLE_KEY; + return typeof t.userInput[etbsSingleKey] === 'undefined'; + } catch (e) { + console.log('Error in retrieving EnketoTripButtonService: ', e); + } +}; const UNLABELED = { - key: "unlabeled", - text: i18next.t("diary.unlabeled"), - filter: unlabeledCheck -} + key: 'unlabeled', + text: i18next.t('diary.unlabeled'), + filter: unlabeledCheck, +}; const TO_LABEL = { - key: "to_label", - text: i18next.t("diary.to-label"), - filter: unlabeledCheck -} + key: 'to_label', + text: i18next.t('diary.to-label'), + filter: unlabeledCheck, +}; -export const configuredFilters = [ - TO_LABEL, - UNLABELED -]; \ No newline at end of file +export const configuredFilters = [TO_LABEL, UNLABELED]; diff --git a/www/js/survey/input-matcher.js b/www/js/survey/input-matcher.js index 2e3d5b908..6fc3178df 100644 --- a/www/js/survey/input-matcher.js +++ b/www/js/survey/input-matcher.js @@ -2,23 +2,37 @@ import angular from 'angular'; -angular.module('emission.survey.inputmatcher', ['emission.plugin.logger']) -.factory('InputMatcher', function(Logger){ - var im = {}; - - const EPOCH_MAXIMUM = 2**31 - 1; - const fmtTs = function(ts_in_secs, tz) { - return moment(ts_in_secs * 1000).tz(tz).format(); - } - - var printUserInput = function(ui) { - return fmtTs(ui.data.start_ts, ui.metadata.time_zone) + "("+ui.data.start_ts + ") -> "+ - fmtTs(ui.data.end_ts, ui.metadata.time_zone) + "("+ui.data.end_ts + ")"+ - " " + ui.data.label + " logged at "+ ui.metadata.write_ts; - } - - im.validUserInputForDraftTrip = function(trip, userInput, logsEnabled) { - if (logsEnabled) { +angular + .module('emission.survey.inputmatcher', ['emission.plugin.logger']) + .factory('InputMatcher', function (Logger) { + var im = {}; + + const EPOCH_MAXIMUM = 2 ** 31 - 1; + const fmtTs = function (ts_in_secs, tz) { + return moment(ts_in_secs * 1000) + .tz(tz) + .format(); + }; + + var printUserInput = function (ui) { + return ( + fmtTs(ui.data.start_ts, ui.metadata.time_zone) + + '(' + + ui.data.start_ts + + ') -> ' + + fmtTs(ui.data.end_ts, ui.metadata.time_zone) + + '(' + + ui.data.end_ts + + ')' + + ' ' + + ui.data.label + + ' logged at ' + + ui.metadata.write_ts + ); + }; + + im.validUserInputForDraftTrip = function (trip, userInput, logsEnabled) { + if (logsEnabled) { Logger.log(`Draft trip: comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} @@ -29,40 +43,40 @@ angular.module('emission.survey.inputmatcher', ['emission.plugin.logger']) || ${-(userInput.data.start_ts - trip.start_ts) <= 15 * 60}) && ${userInput.data.end_ts <= trip.end_ts} `); - } - return (userInput.data.start_ts >= trip.start_ts - && userInput.data.start_ts < trip.end_ts - || -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) - && userInput.data.end_ts <= trip.end_ts; - } - - im.validUserInputForTimelineEntry = function(tlEntry, userInput, logsEnabled) { - if (!tlEntry.origin_key) return false; - if (tlEntry.origin_key.includes('UNPROCESSED') == true) + } + return ( + ((userInput.data.start_ts >= trip.start_ts && userInput.data.start_ts < trip.end_ts) || + -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) && + userInput.data.end_ts <= trip.end_ts + ); + }; + + im.validUserInputForTimelineEntry = function (tlEntry, userInput, logsEnabled) { + if (!tlEntry.origin_key) return false; + if (tlEntry.origin_key.includes('UNPROCESSED') == true) return im.validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); - /* Place-level inputs always have a key starting with 'manual/place', and + /* Place-level inputs always have a key starting with 'manual/place', and trip-level inputs never have a key starting with 'manual/place' So if these don't match, we can immediately return false */ - const entryIsPlace = tlEntry.origin_key == 'analysis/confirmed_place'; - const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); - if (entryIsPlace != isPlaceInput) - return false; - - let entryStart = tlEntry.start_ts || tlEntry.enter_ts; - let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; - if (!entryStart && entryEnd) { - // if a place has no enter time, this is the first start_place of the first composite trip object - // so we will set the start time to the start of the day of the end time for the purpose of comparison - entryStart = moment.unix(entryEnd).startOf('day').unix(); - } - if (!entryEnd) { + const entryIsPlace = tlEntry.origin_key == 'analysis/confirmed_place'; + const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); + if (entryIsPlace != isPlaceInput) return false; + + let entryStart = tlEntry.start_ts || tlEntry.enter_ts; + let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; + if (!entryStart && entryEnd) { + // if a place has no enter time, this is the first start_place of the first composite trip object + // so we will set the start time to the start of the day of the end time for the purpose of comparison + entryStart = moment.unix(entryEnd).startOf('day').unix(); + } + if (!entryEnd) { // if a place has no exit time, the user hasn't left there yet // so we will set the end time as high as possible for the purpose of comparison entryEnd = EPOCH_MAXIMUM; - } - - if (logsEnabled) { + } + + if (logsEnabled) { Logger.log(`Cleaned trip: comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} @@ -73,141 +87,187 @@ angular.module('emission.survey.inputmatcher', ['emission.plugin.logger']) end checks are ${userInput.data.end_ts <= entryEnd} || ${userInput.data.end_ts - entryEnd <= 15 * 60}) `); - } + } - /* For this input to match, it must begin after the start of the timelineEntry (inclusive) + /* For this input to match, it must begin after the start of the timelineEntry (inclusive) but before the end of the timelineEntry (exclusive) */ - const startChecks = userInput.data.start_ts >= entryStart && - userInput.data.start_ts < entryEnd; - /* A matching user input must also finish before the end of the timelineEntry, + const startChecks = + userInput.data.start_ts >= entryStart && userInput.data.start_ts < entryEnd; + /* A matching user input must also finish before the end of the timelineEntry, or within 15 minutes. */ - var endChecks = (userInput.data.end_ts <= entryEnd || - (userInput.data.end_ts - entryEnd) <= 15 * 60); - if (startChecks && !endChecks) { + var endChecks = + userInput.data.end_ts <= entryEnd || userInput.data.end_ts - entryEnd <= 15 * 60; + if (startChecks && !endChecks) { const nextEntryObj = tlEntry.getNextEntry(); if (nextEntryObj) { - const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; - if (!nextEntryEnd) { // the last place will not have an exit_ts - endChecks = true; // so we will just skip the end check - } else { - endChecks = userInput.data.end_ts <= nextEntryEnd; - Logger.log("Second level of end checks when the next trip is defined("+userInput.data.end_ts+" <= "+ nextEntryEnd+") = "+endChecks); - } + const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; + if (!nextEntryEnd) { + // the last place will not have an exit_ts + endChecks = true; // so we will just skip the end check + } else { + endChecks = userInput.data.end_ts <= nextEntryEnd; + Logger.log( + 'Second level of end checks when the next trip is defined(' + + userInput.data.end_ts + + ' <= ' + + nextEntryEnd + + ') = ' + + endChecks, + ); + } } else { - // next trip is not defined, last trip - endChecks = (userInput.data.end_local_dt.day == userInput.data.start_local_dt.day) - Logger.log("Second level of end checks for the last trip of the day"); - Logger.log("compare "+userInput.data.end_local_dt.day + " with " + userInput.data.start_local_dt.day + " = " + endChecks); + // next trip is not defined, last trip + endChecks = userInput.data.end_local_dt.day == userInput.data.start_local_dt.day; + Logger.log('Second level of end checks for the last trip of the day'); + Logger.log( + 'compare ' + + userInput.data.end_local_dt.day + + ' with ' + + userInput.data.start_local_dt.day + + ' = ' + + endChecks, + ); } if (endChecks) { - // If we have flipped the values, check to see that there - // is sufficient overlap - const overlapDuration = Math.min(userInput.data.end_ts, entryEnd) - Math.max(userInput.data.start_ts, entryStart) - Logger.log("Flipped endCheck, overlap("+overlapDuration+ - ")/trip("+tlEntry.duration+") = "+ (overlapDuration / tlEntry.duration)); - endChecks = (overlapDuration/tlEntry.duration) > 0.5; + // If we have flipped the values, check to see that there + // is sufficient overlap + const overlapDuration = + Math.min(userInput.data.end_ts, entryEnd) - + Math.max(userInput.data.start_ts, entryStart); + Logger.log( + 'Flipped endCheck, overlap(' + + overlapDuration + + ')/trip(' + + tlEntry.duration + + ') = ' + + overlapDuration / tlEntry.duration, + ); + endChecks = overlapDuration / tlEntry.duration > 0.5; } - } - return startChecks && endChecks; - } - - // parallels get_not_deleted_candidates() in trip_queries.py - const getNotDeletedCandidates = function(candidates) { - console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); - // We want to retain all ACTIVE entries that have not been DELETED - const allActiveList = candidates.filter(c => !c.data.status || c.data.status == 'ACTIVE'); - const allDeletedIds = candidates.filter(c => c.data.status && c.data.status == 'DELETED').map(c => c.data['match_id']); - const notDeletedActive = allActiveList.filter(c => !allDeletedIds.includes(c.data['match_id'])); - console.log(`Found ${allActiveList.length} active entries, + } + return startChecks && endChecks; + }; + + // parallels get_not_deleted_candidates() in trip_queries.py + const getNotDeletedCandidates = function (candidates) { + console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); + // We want to retain all ACTIVE entries that have not been DELETED + const allActiveList = candidates.filter((c) => !c.data.status || c.data.status == 'ACTIVE'); + const allDeletedIds = candidates + .filter((c) => c.data.status && c.data.status == 'DELETED') + .map((c) => c.data['match_id']); + const notDeletedActive = allActiveList.filter( + (c) => !allDeletedIds.includes(c.data['match_id']), + ); + console.log(`Found ${allActiveList.length} active entries, ${allDeletedIds.length} deleted entries -> ${notDeletedActive.length} non deleted active entries`); - return notDeletedActive; - } + return notDeletedActive; + }; - im.getUserInputForTrip = function(trip, nextTrip, userInputList) { - const logsEnabled = userInputList.length < 20; + im.getUserInputForTrip = function (trip, nextTrip, userInputList) { + const logsEnabled = userInputList.length < 20; - if (userInputList === undefined) { - Logger.log("In getUserInputForTrip, no user input, returning undefined"); + if (userInputList === undefined) { + Logger.log('In getUserInputForTrip, no user input, returning undefined'); return undefined; - } - - if (logsEnabled) { - console.log("Input list = "+userInputList.map(printUserInput)); - } - // undefined != true, so this covers the label view case as well - var potentialCandidates = userInputList.filter((ui) => im.validUserInputForTimelineEntry(trip, ui, logsEnabled)); - if (potentialCandidates.length === 0) { + } + + if (logsEnabled) { + console.log('Input list = ' + userInputList.map(printUserInput)); + } + // undefined != true, so this covers the label view case as well + var potentialCandidates = userInputList.filter((ui) => + im.validUserInputForTimelineEntry(trip, ui, logsEnabled), + ); + if (potentialCandidates.length === 0) { if (logsEnabled) { - Logger.log("In getUserInputForTripStartEnd, no potential candidates, returning []"); + Logger.log('In getUserInputForTripStartEnd, no potential candidates, returning []'); } return undefined; - } + } - if (potentialCandidates.length === 1) { - Logger.log("In getUserInputForTripStartEnd, one potential candidate, returning "+ printUserInput(potentialCandidates[0])); + if (potentialCandidates.length === 1) { + Logger.log( + 'In getUserInputForTripStartEnd, one potential candidate, returning ' + + printUserInput(potentialCandidates[0]), + ); return potentialCandidates[0]; - } + } - Logger.log("potentialCandidates are "+potentialCandidates.map(printUserInput)); - var sortedPC = potentialCandidates.sort(function(pc1, pc2) { + Logger.log('potentialCandidates are ' + potentialCandidates.map(printUserInput)); + var sortedPC = potentialCandidates.sort(function (pc1, pc2) { return pc2.metadata.write_ts - pc1.metadata.write_ts; - }); - var mostRecentEntry = sortedPC[0]; - Logger.log("Returning mostRecentEntry "+printUserInput(mostRecentEntry)); - return mostRecentEntry; - } - - // return array of matching additions for a trip or place - im.getAdditionsForTimelineEntry = function(entry, additionsList) { - const logsEnabled = additionsList.length < 20; - - if (additionsList === undefined) { - Logger.log("In getAdditionsForTimelineEntry, no addition input, returning []"); - return []; - } - - // get additions that have not been deleted - // and filter out additions that do not start within the bounds of the timeline entry - const notDeleted = getNotDeletedCandidates(additionsList); - const matchingAdditions = notDeleted.filter((ui) => im.validUserInputForTimelineEntry(entry, ui, logsEnabled)); - - if (logsEnabled) { - console.log("Matching Addition list = "+matchingAdditions.map(printUserInput)); - } - return matchingAdditions; - } - - im.getUniqueEntries = function(combinedList) { - // we should not get any non-ACTIVE entries here - // since we have run filtering algorithms on both the phone and the server - const allDeleted = combinedList.filter(c => c.data.status && c.data.status == 'DELETED'); - if (allDeleted.length > 0) { - Logger.displayError("Found "+allDeletedEntries.length - +" non-ACTIVE addition entries while trying to dedup entries", - allDeletedEntries); - } - const uniqueMap = new Map(); - combinedList.forEach((e) => { + }); + var mostRecentEntry = sortedPC[0]; + Logger.log('Returning mostRecentEntry ' + printUserInput(mostRecentEntry)); + return mostRecentEntry; + }; + + // return array of matching additions for a trip or place + im.getAdditionsForTimelineEntry = function (entry, additionsList) { + const logsEnabled = additionsList.length < 20; + + if (additionsList === undefined) { + Logger.log('In getAdditionsForTimelineEntry, no addition input, returning []'); + return []; + } + + // get additions that have not been deleted + // and filter out additions that do not start within the bounds of the timeline entry + const notDeleted = getNotDeletedCandidates(additionsList); + const matchingAdditions = notDeleted.filter((ui) => + im.validUserInputForTimelineEntry(entry, ui, logsEnabled), + ); + + if (logsEnabled) { + console.log('Matching Addition list = ' + matchingAdditions.map(printUserInput)); + } + return matchingAdditions; + }; + + im.getUniqueEntries = function (combinedList) { + // we should not get any non-ACTIVE entries here + // since we have run filtering algorithms on both the phone and the server + const allDeleted = combinedList.filter((c) => c.data.status && c.data.status == 'DELETED'); + if (allDeleted.length > 0) { + Logger.displayError( + 'Found ' + + allDeletedEntries.length + + ' non-ACTIVE addition entries while trying to dedup entries', + allDeletedEntries, + ); + } + const uniqueMap = new Map(); + combinedList.forEach((e) => { const existingVal = uniqueMap.get(e.data.match_id); // if the existing entry and the input entry don't match // and they are both active, we have an error // let's notify the user for now if (existingVal) { - if ((existingVal.data.start_ts != e.data.start_ts) || - (existingVal.data.end_ts != e.data.end_ts) || - (existingVal.data.write_ts != e.data.write_ts)) { - Logger.displayError("Found two ACTIVE entries with the same match ID but different timestamps "+existingVal.data.match_id, - JSON.stringify(existingVal) + " vs. "+ JSON.stringify(e)); - } else { - console.log("Found two entries with match_id "+existingVal.data.match_id+" but they are identical"); - } + if ( + existingVal.data.start_ts != e.data.start_ts || + existingVal.data.end_ts != e.data.end_ts || + existingVal.data.write_ts != e.data.write_ts + ) { + Logger.displayError( + 'Found two ACTIVE entries with the same match ID but different timestamps ' + + existingVal.data.match_id, + JSON.stringify(existingVal) + ' vs. ' + JSON.stringify(e), + ); + } else { + console.log( + 'Found two entries with match_id ' + + existingVal.data.match_id + + ' but they are identical', + ); + } } else { - uniqueMap.set(e.data.match_id, e); + uniqueMap.set(e.data.match_id, e); } - }); - return Array.from(uniqueMap.values()); - } + }); + return Array.from(uniqueMap.values()); + }; - return im; -}); + return im; + }); diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index ca71721a7..36a350bd3 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -2,28 +2,38 @@ In the default configuration, these are the "Mode" and "Purpose" buttons. Next to the buttons is a small checkmark icon, which marks inferrel labels as confirmed */ -import React, { useContext, useEffect, useState, useMemo } from "react"; -import { getAngularService } from "../../angular-react-helper"; -import { View, Modal, ScrollView, Pressable, useWindowDimensions } from "react-native"; -import { IconButton, Text, Dialog, useTheme, RadioButton, Button, TextInput } from "react-native-paper"; -import DiaryButton from "../../components/DiaryButton"; -import { useTranslation } from "react-i18next"; -import { LabelTabContext } from "../../diary/LabelTab"; -import { displayErrorMsg, logDebug } from "../../plugin/logger"; -import { getLabelInputDetails, getLabelInputs, readableLabelToKey } from "./confirmHelper"; +import React, { useContext, useEffect, useState, useMemo } from 'react'; +import { getAngularService } from '../../angular-react-helper'; +import { View, Modal, ScrollView, Pressable, useWindowDimensions } from 'react-native'; +import { + IconButton, + Text, + Dialog, + useTheme, + RadioButton, + Button, + TextInput, +} from 'react-native-paper'; +import DiaryButton from '../../components/DiaryButton'; +import { useTranslation } from 'react-i18next'; +import { LabelTabContext } from '../../diary/LabelTab'; +import { displayErrorMsg, logDebug } from '../../plugin/logger'; +import { getLabelInputDetails, getLabelInputs, readableLabelToKey } from './confirmHelper'; -const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { +const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { const { colors } = useTheme(); const { t } = useTranslation(); const { repopulateTimelineEntry, labelOptions } = useContext(LabelTabContext); const { height: windowHeight } = useWindowDimensions(); // modal visible for which input type? (mode or purpose or replaced_mode, null if not visible) - const [ modalVisibleFor, setModalVisibleFor ] = useState<'MODE'|'PURPOSE'|'REPLACED_MODE'|null>(null); - const [otherLabel, setOtherLabel] = useState(null); + const [modalVisibleFor, setModalVisibleFor] = useState< + 'MODE' | 'PURPOSE' | 'REPLACED_MODE' | null + >(null); + const [otherLabel, setOtherLabel] = useState(null); const chosenLabel = useMemo(() => { if (otherLabel != null) return 'other'; - return trip.userInput[modalVisibleFor]?.value + return trip.userInput[modalVisibleFor]?.value; }, [modalVisibleFor, otherLabel]); // to mark 'inferred' labels as 'confirmed'; turn yellow labels blue @@ -51,94 +61,116 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { } function store(inputType, chosenLabel, isOther) { - if (!chosenLabel) return displayErrorMsg("Label is empty"); + if (!chosenLabel) return displayErrorMsg('Label is empty'); if (isOther) { /* Let's make the value for user entered inputs look consistent with our other values (i.e. lowercase, and with underscores instead of spaces) */ chosenLabel = readableLabelToKey(chosenLabel); } const inputDataToStore = { - "start_ts": trip.start_ts, - "end_ts": trip.end_ts, - "label": chosenLabel, + start_ts: trip.start_ts, + end_ts: trip.end_ts, + label: chosenLabel, }; const storageKey = getLabelInputDetails()[inputType].key; window['cordova'].plugins.BEMUserCache.putMessage(storageKey, inputDataToStore).then(() => { dismiss(); repopulateTimelineEntry(trip._id.$oid); - logDebug("Successfully stored input data "+JSON.stringify(inputDataToStore)); + logDebug('Successfully stored input data ' + JSON.stringify(inputDataToStore)); }); } const inputKeys = Object.keys(trip.inputDetails); - return (<> - - - {inputKeys.map((key, i) => { - const input = trip.inputDetails[key]; - const inputIsConfirmed = trip.userInput[input.name]; - const inputIsInferred = trip.finalInference[input.name]; - let fillColor, textColor, borderColor; - if (inputIsConfirmed) { - fillColor = colors.primary; - } else if (inputIsInferred) { - fillColor = colors.secondaryContainer; - borderColor = colors.secondary; - textColor = colors.onSecondaryContainer; - } - const btnText = inputIsConfirmed?.text || inputIsInferred?.text || input.choosetext; + return ( + <> + + + {inputKeys.map((key, i) => { + const input = trip.inputDetails[key]; + const inputIsConfirmed = trip.userInput[input.name]; + const inputIsInferred = trip.finalInference[input.name]; + let fillColor, textColor, borderColor; + if (inputIsConfirmed) { + fillColor = colors.primary; + } else if (inputIsInferred) { + fillColor = colors.secondaryContainer; + borderColor = colors.secondary; + textColor = colors.onSecondaryContainer; + } + const btnText = inputIsConfirmed?.text || inputIsInferred?.text || input.choosetext; - return ( - - {t(input.labeltext)} - setModalVisibleFor(input.name)}> - { t(btnText) } - - - ) - })} - - {trip.verifiability === 'can-verify' && ( - - + return ( + + {t(input.labeltext)} + setModalVisibleFor(input.name)}> + {t(btnText)} + + + ); + })} - )} - - dismiss()}> - dismiss()}> - - - {(modalVisibleFor == 'MODE') && t('diary.select-mode-scroll') || - (modalVisibleFor == 'PURPOSE') && t('diary.select-purpose-scroll') || - (modalVisibleFor == 'REPLACED_MODE') && t('diary.select-replaced-mode-scroll')} - - - - onChooseLabel(val)} value={chosenLabel}> - {labelOptions?.[modalVisibleFor]?.map((o, i) => ( - // @ts-ignore - - ))} - - - - {otherLabel != null && <> - setOtherLabel(t)} /> - - - - } - - - - ); + {trip.verifiability === 'can-verify' && ( + + + + )} + + dismiss()}> + dismiss()}> + + + {(modalVisibleFor == 'MODE' && t('diary.select-mode-scroll')) || + (modalVisibleFor == 'PURPOSE' && t('diary.select-purpose-scroll')) || + (modalVisibleFor == 'REPLACED_MODE' && t('diary.select-replaced-mode-scroll'))} + + + + onChooseLabel(val)} value={chosenLabel}> + {labelOptions?.[modalVisibleFor]?.map((o, i) => ( + // @ts-ignore + + ))} + + + + {otherLabel != null && ( + <> + setOtherLabel(t)} + /> + + + + + )} + + + + + ); }; export default MultilabelButtonGroup; diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index b668669bf..a8972709b 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -1,34 +1,36 @@ // may refactor this into a React hook once it's no longer used by any Angular screens -import { getAngularService } from "../../angular-react-helper"; -import { fetchUrlCached } from "../../commHelper"; -import i18next from "i18next"; -import { logDebug } from "../../plugin/logger"; +import { getAngularService } from '../../angular-react-helper'; +import { fetchUrlCached } from '../../commHelper'; +import i18next from 'i18next'; +import { logDebug } from '../../plugin/logger'; type InputDetails = { [k in T]?: { - name: string, - labeltext: string, - choosetext: string, - key: string, - } + name: string; + labeltext: string; + choosetext: string; + key: string; + }; }; -export type LabelOptions = { +export type LabelOptions = { [k in T]: { - value: string, - baseMode: string, - met?: {range: any[], mets: number} - met_equivalent?: string, - kgCo2PerKm: number, - text?: string, - }[] -} & { translations: { - [lang: string]: { [translationKey: string]: string } -}}; + value: string; + baseMode: string; + met?: { range: any[]; mets: number }; + met_equivalent?: string; + kgCo2PerKm: number; + text?: string; + }[]; +} & { + translations: { + [lang: string]: { [translationKey: string]: string }; + }; +}; let appConfig; -export let labelOptions: LabelOptions<'MODE'|'PURPOSE'|'REPLACED_MODE'>; -export let inputDetails: InputDetails<'MODE'|'PURPOSE'|'REPLACED_MODE'>; +export let labelOptions: LabelOptions<'MODE' | 'PURPOSE' | 'REPLACED_MODE'>; +export let inputDetails: InputDetails<'MODE' | 'PURPOSE' | 'REPLACED_MODE'>; export async function getLabelOptions(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; @@ -36,11 +38,15 @@ export async function getLabelOptions(appConfigParam?) { if (appConfig.label_options) { const labelOptionsJson = await fetchUrlCached(appConfig.label_options); - logDebug("label_options found in config, using dynamic label options at " + appConfig.label_options); + logDebug( + 'label_options found in config, using dynamic label options at ' + appConfig.label_options, + ); labelOptions = JSON.parse(labelOptionsJson) as LabelOptions; } else { const defaultLabelOptionsURL = 'json/label-options.json.sample'; - logDebug("No label_options found in config, using default label options at " + defaultLabelOptionsURL); + logDebug( + 'No label_options found in config, using default label options at ' + defaultLabelOptionsURL, + ); const defaultLabelOptionsJson = await fetchUrlCached(defaultLabelOptionsURL); labelOptions = JSON.parse(defaultLabelOptionsJson) as LabelOptions; } @@ -51,7 +57,10 @@ export async function getLabelOptions(appConfigParam?) { labelOptions[opt]?.forEach?.((o, i) => { const translationKey = o.value; // If translation exists in labelOptions, use that. Otherwise, use the one in the i18next. If there is not "translations" field in labelOptions, defaultly use the one in the i18next. - const translation = labelOptions.translations ? labelOptions.translations[lang][translationKey] || i18next.t(`multilabel.${translationKey}`) : i18next.t(`multilabel.${translationKey}`); + const translation = labelOptions.translations + ? labelOptions.translations[lang][translationKey] || + i18next.t(`multilabel.${translationKey}`) + : i18next.t(`multilabel.${translationKey}`); labelOptions[opt][i].text = translation; }); } @@ -60,18 +69,18 @@ export async function getLabelOptions(appConfigParam?) { export const baseLabelInputDetails = { MODE: { - name: "MODE", - labeltext: "diary.mode", - choosetext: "diary.choose-mode", - key: "manual/mode_confirm", + name: 'MODE', + labeltext: 'diary.mode', + choosetext: 'diary.choose-mode', + key: 'manual/mode_confirm', }, PURPOSE: { - name: "PURPOSE", - labeltext: "diary.purpose", - choosetext: "diary.choose-purpose", - key: "manual/purpose_confirm", + name: 'PURPOSE', + labeltext: 'diary.purpose', + choosetext: 'diary.choose-purpose', + key: 'manual/purpose_confirm', }, -} +}; export function getLabelInputDetails(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; @@ -83,13 +92,14 @@ export function getLabelInputDetails(appConfigParam?) { return baseLabelInputDetails; } // else this is a program, so add the REPLACED_MODE - inputDetails = { ...baseLabelInputDetails, + inputDetails = { + ...baseLabelInputDetails, REPLACED_MODE: { - name: "REPLACED_MODE", - labeltext: "diary.replaces", - choosetext: "diary.choose-replaced-mode", - key: "manual/replaced_mode", - } + name: 'REPLACED_MODE', + labeltext: 'diary.replaces', + choosetext: 'diary.choose-replaced-mode', + key: 'manual/replaced_mode', + }, }; return inputDetails; } @@ -99,16 +109,14 @@ export const getBaseLabelInputs = () => Object.keys(baseLabelInputDetails); /** @description replace all underscores with spaces, and capitalizes the first letter of each word */ export const labelKeyToReadable = (otherValue: string) => { - const words = otherValue.replace(/_/g, " ").trim().split(" "); - if (words.length == 0) return ""; - return words.map((word) => - word[0].toUpperCase() + word.slice(1) - ).join(" "); -} + const words = otherValue.replace(/_/g, ' ').trim().split(' '); + if (words.length == 0) return ''; + return words.map((word) => word[0].toUpperCase() + word.slice(1)).join(' '); +}; /** @description replaces all spaces with underscores, and lowercases the string */ export const readableLabelToKey = (otherText: string) => - otherText.trim().replace(/ /g, "_").toLowerCase(); + otherText.trim().replace(/ /g, '_').toLowerCase(); export const getFakeEntry = (otherValue) => ({ text: labelKeyToReadable(otherValue), @@ -116,4 +124,4 @@ export const getFakeEntry = (otherValue) => ({ }); export const labelKeyToRichMode = (labelKey: string) => - labelOptions?.MODE?.find(m => m.value == labelKey)?.text || labelKeyToReadable(labelKey); + labelOptions?.MODE?.find((m) => m.value == labelKey)?.text || labelKeyToReadable(labelKey); diff --git a/www/js/survey/multilabel/infinite_scroll_filters.ts b/www/js/survey/multilabel/infinite_scroll_filters.ts index 8d71266d9..28d91d48d 100644 --- a/www/js/survey/multilabel/infinite_scroll_filters.ts +++ b/www/js/survey/multilabel/infinite_scroll_filters.ts @@ -6,53 +6,52 @@ * All UI elements should only use $scope variables. */ -import i18next from "i18next"; +import i18next from 'i18next'; const unlabeledCheck = (t) => { - return t.INPUTS - .map((inputType, index) => !t.userInput[inputType]) - .reduce((acc, val) => acc || val, false); -} + return t.INPUTS.map((inputType, index) => !t.userInput[inputType]).reduce( + (acc, val) => acc || val, + false, + ); +}; const invalidCheck = (t) => { - const retVal = - (t.userInput['MODE'] && t.userInput['MODE'].value === 'pilot_ebike') && - (!t.userInput['REPLACED_MODE'] || - t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || - t.userInput['REPLACED_MODE'].value === 'same_mode'); - return retVal; -} + const retVal = + t.userInput['MODE'] && + t.userInput['MODE'].value === 'pilot_ebike' && + (!t.userInput['REPLACED_MODE'] || + t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || + t.userInput['REPLACED_MODE'].value === 'same_mode'); + return retVal; +}; const toLabelCheck = (trip) => { - if (trip.expectation) { - console.log(trip.expectation.to_label) - return trip.expectation.to_label && unlabeledCheck(trip); - } else { - return true; - } -} + if (trip.expectation) { + console.log(trip.expectation.to_label); + return trip.expectation.to_label && unlabeledCheck(trip); + } else { + return true; + } +}; const UNLABELED = { - key: "unlabeled", - text: i18next.t("diary.unlabeled"), - filter: unlabeledCheck, - width: "col-50" -} + key: 'unlabeled', + text: i18next.t('diary.unlabeled'), + filter: unlabeledCheck, + width: 'col-50', +}; const INVALID_EBIKE = { - key: "invalid_ebike", - text: i18next.t("diary.invalid-ebike"), - filter: invalidCheck -} + key: 'invalid_ebike', + text: i18next.t('diary.invalid-ebike'), + filter: invalidCheck, +}; const TO_LABEL = { - key: "to_label", - text: i18next.t("diary.to-label"), - filter: toLabelCheck, - width: "col-50" -} - -export const configuredFilters = [ - TO_LABEL, - UNLABELED -]; \ No newline at end of file + key: 'to_label', + text: i18next.t('diary.to-label'), + filter: toLabelCheck, + width: 'col-50', +}; + +export const configuredFilters = [TO_LABEL, UNLABELED]; diff --git a/www/js/survey/multilabel/multi-label-ui.js b/www/js/survey/multilabel/multi-label-ui.js index 7d1bc4007..c4a8c732c 100644 --- a/www/js/survey/multilabel/multi-label-ui.js +++ b/www/js/survey/multilabel/multi-label-ui.js @@ -1,205 +1,240 @@ import angular from 'angular'; -import { baseLabelInputDetails, getBaseLabelInputs, getFakeEntry, getLabelInputDetails, getLabelInputs, getLabelOptions } from './confirmHelper'; +import { + baseLabelInputDetails, + getBaseLabelInputs, + getFakeEntry, + getLabelInputDetails, + getLabelInputs, + getLabelOptions, +} from './confirmHelper'; import { getConfig } from '../../config/dynamicConfig'; -angular.module('emission.survey.multilabel.buttons', - ['emission.survey.inputmatcher']) - -.factory("MultiLabelService", function($rootScope, InputMatcher, $timeout, $ionicPlatform, Logger) { - var mls = {}; - console.log("Creating MultiLabelService"); - mls.init = function(config) { - Logger.log("About to initialize the MultiLabelService"); - mls.ui_config = config; - getLabelOptions(config).then((inputParams) => mls.inputParams = inputParams); - mls.MANUAL_KEYS = Object.values(getLabelInputDetails(config)).map((inp) => inp.key); - Logger.log("finished initializing the MultiLabelService"); - }; - - $ionicPlatform.ready().then(function() { - Logger.log("UI_CONFIG: about to call configReady function in MultiLabelService"); - getConfig().then((newConfig) => { - mls.init(newConfig); - }).catch((err) => Logger.displayError("Error while handling config in MultiLabelService", err)); - }); - - /** - * Embed 'inputType' to the trip. - */ - - mls.extractResult = (results) => results; - - mls.processManualInputs = function(manualResults, resultMap) { - var mrString = 'unprocessed manual inputs ' - + manualResults.map(function(item, index) { - return ` ${item.length} ${getLabelInputs()[index]}`; - }); - console.log(mrString); - manualResults.forEach(function(mr, index) { - resultMap[getLabelInputs()[index]] = mr; +angular + .module('emission.survey.multilabel.buttons', ['emission.survey.inputmatcher']) + + .factory( + 'MultiLabelService', + function ($rootScope, InputMatcher, $timeout, $ionicPlatform, Logger) { + var mls = {}; + console.log('Creating MultiLabelService'); + mls.init = function (config) { + Logger.log('About to initialize the MultiLabelService'); + mls.ui_config = config; + getLabelOptions(config).then((inputParams) => (mls.inputParams = inputParams)); + mls.MANUAL_KEYS = Object.values(getLabelInputDetails(config)).map((inp) => inp.key); + Logger.log('finished initializing the MultiLabelService'); + }; + + $ionicPlatform.ready().then(function () { + Logger.log('UI_CONFIG: about to call configReady function in MultiLabelService'); + getConfig() + .then((newConfig) => { + mls.init(newConfig); + }) + .catch((err) => + Logger.displayError('Error while handling config in MultiLabelService', err), + ); }); - } - - mls.populateInputsAndInferences = function(trip, manualResultMap) { - if (angular.isDefined(trip)) { - // console.log("Expectation: "+JSON.stringify(trip.expectation)); - // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); - trip.userInput = {}; - getLabelInputs().forEach(function(item, index) { - mls.populateManualInputs(trip, trip.nextTrip, item, - manualResultMap[item]); + + /** + * Embed 'inputType' to the trip. + */ + + mls.extractResult = (results) => results; + + mls.processManualInputs = function (manualResults, resultMap) { + var mrString = + 'unprocessed manual inputs ' + + manualResults.map(function (item, index) { + return ` ${item.length} ${getLabelInputs()[index]}`; + }); + console.log(mrString); + manualResults.forEach(function (mr, index) { + resultMap[getLabelInputs()[index]] = mr; }); - trip.finalInference = {}; - mls.inferFinalLabels(trip); - mls.expandInputsIfNecessary(trip); - mls.updateVerifiability(trip); - } else { - console.log("Trip information not yet bound, skipping fill"); - } - } - - /** - * Embed 'inputType' to the trip - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { - // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, - inputList); - var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined; - if (!angular.isDefined(userInputLabel)) { + }; + + mls.populateInputsAndInferences = function (trip, manualResultMap) { + if (angular.isDefined(trip)) { + // console.log("Expectation: "+JSON.stringify(trip.expectation)); + // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); + trip.userInput = {}; + getLabelInputs().forEach(function (item, index) { + mls.populateManualInputs(trip, trip.nextTrip, item, manualResultMap[item]); + }); + trip.finalInference = {}; + mls.inferFinalLabels(trip); + mls.expandInputsIfNecessary(trip); + mls.updateVerifiability(trip); + } else { + console.log('Trip information not yet bound, skipping fill'); + } + }; + + /** + * Embed 'inputType' to the trip + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { + // Check unprocessed labels first since they are more recent + const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, inputList); + var userInputLabel = unprocessedLabelEntry ? unprocessedLabelEntry.data.label : undefined; + if (!angular.isDefined(userInputLabel)) { userInputLabel = trip.user_input?.[mls.inputType2retKey(inputType)]; - } - mls.populateInput(trip.userInput, inputType, userInputLabel); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - mls.editingTrip = angular.undefined; - } - - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - mls.populateInput = function(tripField, inputType, userInputLabel) { - if (angular.isDefined(userInputLabel)) { - console.log("populateInput: looking in map of "+inputType+" for userInputLabel"+userInputLabel); - var userInputEntry = mls.inputParams[inputType].find(o => o.value == userInputLabel); - if (!angular.isDefined(userInputEntry)) { - userInputEntry = getFakeEntry(userInputLabel); - mls.inputParams[inputType].push(userInputEntry); } - console.log("Mapped label "+userInputLabel+" to entry "+JSON.stringify(userInputEntry)); - tripField[inputType] = userInputEntry; - } - } - - /** - * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. - * The algorithm below operationalizes these principles: - * - Never consider label tuples that contradict a green label - * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before - * - After filtering, predict the most likely choices at the level of individual labels, not label tuples - * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold - */ - mls.inferFinalLabels = function(trip) { - // Deep copy the possibility tuples - let labelsList = []; - if (angular.isDefined(trip.inferred_labels)) { - labelsList = JSON.parse(JSON.stringify(trip.inferred_labels)); - } - - // Capture the level of certainty so we can reconstruct it later - const totalCertainty = labelsList.map(item => item.p).reduce(((item, rest) => item + rest), 0); - - // Filter out the tuples that are inconsistent with existing green labels - for (const inputType of getLabelInputs()) { - const userInput = trip.userInput[inputType]; - if (userInput) { - const retKey = mls.inputType2retKey(inputType); - labelsList = labelsList.filter(item => item.labels[retKey] == userInput.value); - } - } - - // Red labels if we have no possibilities left - if (labelsList.length == 0) { - for (const inputType of getLabelInputs()) mls.populateInput(trip.finalInference, inputType, undefined); - } - else { - // Normalize probabilities to previous level of certainty - const certaintyScalar = totalCertainty/labelsList.map(item => item.p).reduce((item, rest) => item + rest); - labelsList.forEach(item => item.p*=certaintyScalar); - - for (const inputType of getLabelInputs()) { - // For each label type, find the most probable value by binning by label value and summing - const retKey = mls.inputType2retKey(inputType); - let valueProbs = new Map(); - for (const tuple of labelsList) { - const labelValue = tuple.labels[retKey]; - if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0); - valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p); + mls.populateInput(trip.userInput, inputType, userInputLabel); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + mls.editingTrip = angular.undefined; + }; + + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + mls.populateInput = function (tripField, inputType, userInputLabel) { + if (angular.isDefined(userInputLabel)) { + console.log( + 'populateInput: looking in map of ' + + inputType + + ' for userInputLabel' + + userInputLabel, + ); + var userInputEntry = mls.inputParams[inputType].find((o) => o.value == userInputLabel); + if (!angular.isDefined(userInputEntry)) { + userInputEntry = getFakeEntry(userInputLabel); + mls.inputParams[inputType].push(userInputEntry); + } + console.log( + 'Mapped label ' + userInputLabel + ' to entry ' + JSON.stringify(userInputEntry), + ); + tripField[inputType] = userInputEntry; } - let max = {p: 0, labelValue: undefined}; - for (const [thisLabelValue, thisP] of valueProbs) { - // In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order) - if (thisP > max.p) max = {p: thisP, labelValue: thisLabelValue}; + }; + + /** + * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. + * The algorithm below operationalizes these principles: + * - Never consider label tuples that contradict a green label + * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before + * - After filtering, predict the most likely choices at the level of individual labels, not label tuples + * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold + */ + mls.inferFinalLabels = function (trip) { + // Deep copy the possibility tuples + let labelsList = []; + if (angular.isDefined(trip.inferred_labels)) { + labelsList = JSON.parse(JSON.stringify(trip.inferred_labels)); } - // Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold - // Fails safe if confidence_threshold doesn't exist - if (max.p <= trip.confidence_threshold) max.labelValue = undefined; - - mls.populateInput(trip.finalInference, inputType, max.labelValue); - } - } - } - - /* - * Uses either 2 or 3 labels depending on the type of install (program vs. study) - * and the primary mode. - * This used to be in the controller, where it really should be, but we had - * to move it to the service because we need to invoke it from the list view - * as part of filtering "To Label" entries. - * - * TODO: Move it back later after the diary vs. label unification - */ - mls.expandInputsIfNecessary = function(trip) { - console.log("Reading expanding inputs for ", trip); - const inputValue = trip.userInput["MODE"]? trip.userInput["MODE"].value : undefined; - console.log("Experimenting with expanding inputs for mode "+inputValue); - if (mls.ui_config.intro.mode_studied) { - if (inputValue == mls.ui_config.intro.mode_studied) { - Logger.log("Found "+mls.ui_config.intro.mode_studied+" mode in a program, displaying full details"); + // Capture the level of certainty so we can reconstruct it later + const totalCertainty = labelsList + .map((item) => item.p) + .reduce((item, rest) => item + rest, 0); + + // Filter out the tuples that are inconsistent with existing green labels + for (const inputType of getLabelInputs()) { + const userInput = trip.userInput[inputType]; + if (userInput) { + const retKey = mls.inputType2retKey(inputType); + labelsList = labelsList.filter((item) => item.labels[retKey] == userInput.value); + } + } + + // Red labels if we have no possibilities left + if (labelsList.length == 0) { + for (const inputType of getLabelInputs()) + mls.populateInput(trip.finalInference, inputType, undefined); + } else { + // Normalize probabilities to previous level of certainty + const certaintyScalar = + totalCertainty / labelsList.map((item) => item.p).reduce((item, rest) => item + rest); + labelsList.forEach((item) => (item.p *= certaintyScalar)); + + for (const inputType of getLabelInputs()) { + // For each label type, find the most probable value by binning by label value and summing + const retKey = mls.inputType2retKey(inputType); + let valueProbs = new Map(); + for (const tuple of labelsList) { + const labelValue = tuple.labels[retKey]; + if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0); + valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p); + } + let max = { p: 0, labelValue: undefined }; + for (const [thisLabelValue, thisP] of valueProbs) { + // In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order) + if (thisP > max.p) max = { p: thisP, labelValue: thisLabelValue }; + } + + // Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold + // Fails safe if confidence_threshold doesn't exist + if (max.p <= trip.confidence_threshold) max.labelValue = undefined; + + mls.populateInput(trip.finalInference, inputType, max.labelValue); + } + } + }; + + /* + * Uses either 2 or 3 labels depending on the type of install (program vs. study) + * and the primary mode. + * This used to be in the controller, where it really should be, but we had + * to move it to the service because we need to invoke it from the list view + * as part of filtering "To Label" entries. + * + * TODO: Move it back later after the diary vs. label unification + */ + mls.expandInputsIfNecessary = function (trip) { + console.log('Reading expanding inputs for ', trip); + const inputValue = trip.userInput['MODE'] ? trip.userInput['MODE'].value : undefined; + console.log('Experimenting with expanding inputs for mode ' + inputValue); + if (mls.ui_config.intro.mode_studied) { + if (inputValue == mls.ui_config.intro.mode_studied) { + Logger.log( + 'Found ' + + mls.ui_config.intro.mode_studied + + ' mode in a program, displaying full details', + ); trip.inputDetails = getLabelInputDetails(); trip.INPUTS = getLabelInputs(); - } else { - Logger.log("Found non "+mls.ui_config.intro.mode_studied+" mode in a program, displaying base details"); + } else { + Logger.log( + 'Found non ' + + mls.ui_config.intro.mode_studied + + ' mode in a program, displaying base details', + ); trip.inputDetails = baseLabelInputDetails; trip.INPUTS = getBaseLabelInputs(); + } + } else { + Logger.log('study, not program, displaying full details'); + trip.INPUTS = getLabelInputs(); + trip.inputDetails = getLabelInputDetails(); + } + }; + + /** + * MODE (becomes manual/mode_confirm) becomes mode_confirm + */ + mls.inputType2retKey = function (inputType) { + return getLabelInputDetails()[inputType].key.split('/')[1]; + }; + + mls.updateVerifiability = function (trip) { + var allGreen = true; + var someYellow = false; + for (const inputType of trip.INPUTS) { + const green = trip.userInput[inputType]; + const yellow = trip.finalInference[inputType] && !green; + if (yellow) someYellow = true; + if (!green) allGreen = false; } - } else { - Logger.log("study, not program, displaying full details"); - trip.INPUTS = getLabelInputs(); - trip.inputDetails = getLabelInputDetails(); - } - } - - /** - * MODE (becomes manual/mode_confirm) becomes mode_confirm - */ - mls.inputType2retKey = function(inputType) { - return getLabelInputDetails()[inputType].key.split("/")[1]; - } - - mls.updateVerifiability = function(trip) { - var allGreen = true; - var someYellow = false; - for (const inputType of trip.INPUTS) { - const green = trip.userInput[inputType]; - const yellow = trip.finalInference[inputType] && !green; - if (yellow) someYellow = true; - if (!green) allGreen = false; - } - trip.verifiability = someYellow ? "can-verify" : (allGreen ? "already-verified" : "cannot-verify"); - } - - return mls; -}); + trip.verifiability = someYellow + ? 'can-verify' + : allGreen + ? 'already-verified' + : 'cannot-verify'; + }; + + return mls; + }, + ); diff --git a/www/js/survey/survey.ts b/www/js/survey/survey.ts index 66f662082..a12e65713 100644 --- a/www/js/survey/survey.ts +++ b/www/js/survey/survey.ts @@ -1,16 +1,16 @@ -import { configuredFilters as multilabelConfiguredFilters } from "./multilabel/infinite_scroll_filters"; -import { configuredFilters as enketoConfiguredFilters } from "./enketo/infinite_scroll_filters"; +import { configuredFilters as multilabelConfiguredFilters } from './multilabel/infinite_scroll_filters'; +import { configuredFilters as enketoConfiguredFilters } from './enketo/infinite_scroll_filters'; -type SurveyOption = { filter: Array, service: string, elementTag: string } -export const SurveyOptions: {[key: string]: SurveyOption} = { +type SurveyOption = { filter: Array; service: string; elementTag: string }; +export const SurveyOptions: { [key: string]: SurveyOption } = { MULTILABEL: { filter: multilabelConfiguredFilters, - service: "MultiLabelService", - elementTag: "multilabel" + service: 'MultiLabelService', + elementTag: 'multilabel', }, ENKETO: { filter: enketoConfiguredFilters, - service: "EnketoTripButtonService", - elementTag: "enketo-trip-button" - } -} + service: 'EnketoTripButtonService', + elementTag: 'enketo-trip-button', + }, +}; diff --git a/www/js/useAppConfig.ts b/www/js/useAppConfig.ts index 633069326..96d1a56cb 100644 --- a/www/js/useAppConfig.ts +++ b/www/js/useAppConfig.ts @@ -1,10 +1,9 @@ -import { useEffect, useState } from "react"; -import { getAngularService } from "./angular-react-helper" -import { configChanged, getConfig, setConfigChanged } from "./config/dynamicConfig"; -import { logDebug } from "./plugin/logger"; +import { useEffect, useState } from 'react'; +import { getAngularService } from './angular-react-helper'; +import { configChanged, getConfig, setConfigChanged } from './config/dynamicConfig'; +import { logDebug } from './plugin/logger'; const useAppConfig = () => { - const [appConfig, setAppConfig] = useState(null); const $ionicPlatform = getAngularService('$ionicPlatform'); @@ -27,6 +26,6 @@ const useAppConfig = () => { updateConfig().then(() => setConfigChanged(false)); } return appConfig; -} +}; export default useAppConfig; diff --git a/www/js/useAppStateChange.ts b/www/js/useAppStateChange.ts index 8b9c6497c..470eb67a6 100644 --- a/www/js/useAppStateChange.ts +++ b/www/js/useAppStateChange.ts @@ -7,23 +7,20 @@ import { useEffect, useRef } from 'react'; import { AppState } from 'react-native'; const useAppStateChange = (onResume) => { + const appState = useRef(AppState.currentState); - const appState = useRef(AppState.currentState); - - useEffect(() => { - const subscription = AppState.addEventListener('change', nextAppState => { - if ( appState.current != 'active' && nextAppState === 'active') { - onResume(); - } - - appState.current = nextAppState; - console.log('AppState', appState.current); - }); - - }, []); - - return {}; - } - - export default useAppStateChange; - \ No newline at end of file + useEffect(() => { + const subscription = AppState.addEventListener('change', (nextAppState) => { + if (appState.current != 'active' && nextAppState === 'active') { + onResume(); + } + + appState.current = nextAppState; + console.log('AppState', appState.current); + }); + }, []); + + return {}; +}; + +export default useAppStateChange; diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts index 035ba6b16..1bef38c44 100644 --- a/www/js/usePermissionStatus.ts +++ b/www/js/usePermissionStatus.ts @@ -1,352 +1,434 @@ import { useEffect, useState, useMemo } from 'react'; -import useAppStateChange from "./useAppStateChange"; -import useAppConfig from "./useAppConfig"; +import useAppStateChange from './useAppStateChange'; +import useAppConfig from './useAppConfig'; import { useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; //refreshing checks with the plugins to update the check's statusState export function refreshAllChecks(checkList) { - //refresh each check - checkList.forEach((lc) => { - lc.refresh(); - }); - console.log("setting checks are", checkList); + //refresh each check + checkList.forEach((lc) => { + lc.refresh(); + }); + console.log('setting checks are', checkList); } const usePermissionStatus = () => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const appConfig = useAppConfig(); + + const [error, setError] = useState(''); + const [errorVis, setErrorVis] = useState(false); - const { t } = useTranslation(); - const { colors } = useTheme(); - const appConfig = useAppConfig(); + const [checkList, setCheckList] = useState([]); + const [explanationList, setExplanationList] = useState>([]); + const [haveSetText, setHaveSetText] = useState(false); - const [error, setError] = useState(""); - const [errorVis, setErrorVis] = useState(false); + let iconMap = (statusState) => (statusState ? 'check-circle-outline' : 'alpha-x-circle-outline'); + let colorMap = (statusState) => (statusState ? colors.success : colors.danger); - const [checkList, setCheckList] = useState([]); - const [explanationList, setExplanationList] = useState>([]); - const [haveSetText, setHaveSetText] = useState(false); + 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]); - let iconMap = (statusState) => statusState ? "check-circle-outline" : "alpha-x-circle-outline"; - let colorMap = (statusState) => statusState ? colors.success : colors.danger; + //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 + //update the visiblility pieces here, rather than mutating + newObject.statusIcon = iconMap(newObject.statusState); + newObject.statusColor = colorMap(newObject.statusState); + //"find and replace" the check + tempList.forEach((item, i) => { + if (item.name == newObject.name) { + tempList[i] = newObject; + } + }); + setCheckList(tempList); + } - 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; - } - }) + 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; - }, [checkList]) + }) + .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; + }); + } - //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 - //update the visiblility pieces here, rather than mutating - newObject.statusIcon = iconMap(newObject.statusState); - newObject.statusColor = colorMap(newObject.statusState); - //"find and replace" the check - tempList.forEach((item, i) => { - if(item.name == newObject.name){ - tempList[i] = newObject; - } - }); - setCheckList(tempList); + 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 (window['device'].version.split('.')[0] < 9) { + androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-lt-9'; } - - 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; - }); + var androidPermDescTag = 'intro.appstatus.locperms.description.android-gte-12'; + if (window['device'].version.split('.')[0] < 6) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; + } else if (window['device'].version.split('.')[0] < 10) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-6-9'; + } else if (window['device'].version.split('.')[0] < 11) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-10'; + } else if (window['device'].version.split('.')[0] < 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); + } - 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 (window['device'].version.split(".")[0] < 9) { - androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; - } - var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; - if(window['device'].version.split(".")[0] < 6) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; - } else if (window['device'].version.split(".")[0] < 10) { - androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; - } else if (window['device'].version.split(".")[0] < 11) { - androidPermDescTag= "intro.appstatus.locperms.description.android-10"; - } else if (window['device'].version.split(".")[0] < 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 + 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 locPermissionsCheck = { - name: t("intro.appstatus.locperms.name"), - desc: t(androidPermDescTag), - fix: fixPerms, - refresh: checkPerms - } - let tempChecks = checkList; - tempChecks.push(locSettingsCheck, locPermissionsCheck); - setCheckList(tempChecks); + }); + }; + 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 (window['device'].version.split('.')[0] < 13) { + iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; } + console.log('description tags are ' + iOSSettingsDescTag + ' ' + iOSPermDescTag); - 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(window['device'].version.split(".")[0] < 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); + } - 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 (window['device'].version.split('.')[0] >= 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, + ); + }; - function setupAndroidFitnessChecks() { - if(window['device'].version.split(".")[0] >= 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); - } + 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}}); - }; - 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 + 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 tempChecks = checkList; - tempChecks.push(fitnessPermissionsCheck); - setCheckList(tempChecks); - } + }); + }; + let checkPerms = function () { + console.log('fix and refresh fitness permissions'); + return checkOrFix( + fitnessPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false, + ); + }; - 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); + 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 (window['device'].version.split('.')[0] == 12) { + androidUnusedDescTag = 'intro.appstatus.unusedapprestrict.description.android-disable-12'; + } else if (window['device'].version.split('.')[0] < 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'), + fix: fixBatteryOpt, + refresh: checkBatteryOpt, + }; + let tempChecks = checkList; + tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck); + 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 (window['device'].version.split(".")[0] == 12) { - androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-12"; - } - else if (window['device'].version.split(".")[0] < 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"), - 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 (window['device'].platform.toLowerCase() == 'ios') { + overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); } + 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'), + }); - function setupPermissionText() { - let tempExplanations = explanationList; + setExplanationList(tempExplanations); - let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); - let locExplanation = t('intro.appstatus.overall-loc-description'); - if(window['device'].platform.toLowerCase() == "ios") { - overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); - } - 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')}); + //TODO - update samsung handling based on feedback - setExplanationList(tempExplanations); - - //TODO - update samsung handling based on feedback + console.log('Explanation = ' + explanationList); + } - console.log("Explanation = "+explanationList); + function createChecklist() { + if (window['device'].platform.toLowerCase() == 'android') { + setupAndroidLocChecks(); + setupAndroidFitnessChecks(); + setupAndroidNotificationChecks(); + setupAndroidBackgroundRestrictionChecks(); + } else if (window['device'].platform.toLowerCase() == '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 createChecklist(){ - if(window['device'].platform.toLowerCase() == "android") { - setupAndroidLocChecks(); - setupAndroidFitnessChecks(); - setupAndroidNotificationChecks(); - setupAndroidBackgroundRestrictionChecks(); - } else if (window['device'].platform.toLowerCase() == "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(checkList); + refreshAllChecks(checkList); + } + + useAppStateChange(function () { + console.log('PERMISSION CHECK: app has resumed, should refresh'); + refreshAllChecks(checkList); + }); + + //load when ready + useEffect(() => { + if (appConfig && window['device']?.platform) { + setupPermissionText(); + setHaveSetText(true); + console.log('setting up permissions'); + createChecklist(); } + }, [appConfig]); - useAppStateChange( function() { - console.log("PERMISSION CHECK: app has resumed, should refresh"); - refreshAllChecks(checkList); - }); + return { checkList, overallStatus, error, errorVis, setErrorVis, explanationList }; +}; - //load when ready - useEffect(() => { - if (appConfig && window['device']?.platform) { - setupPermissionText(); - setHaveSetText(true); - console.log("setting up permissions"); - createChecklist(); - } - }, [appConfig]); - - return {checkList, overallStatus, error, errorVis, setErrorVis, explanationList}; - } - - export default usePermissionStatus; +export default usePermissionStatus;