From aaaf49b9bf38bce1d2115b0dd08bee5098b8023a Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Fri, 2 Feb 2024 19:45:17 +0700 Subject: [PATCH 01/22] [#1076] Add new UserState: currentLocation & locationIsGranted currentLocation = Store current location from watchPositionAsync result locationIsGranted = Store access location permission (granted = True, otherwise = False) --- app/src/store/users.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/store/users.js b/app/src/store/users.js index 9139c2d89..2cf5867fd 100644 --- a/app/src/store/users.js +++ b/app/src/store/users.js @@ -8,6 +8,8 @@ const UserState = new Store({ syncWifiOnly: false, // syncInterval: 300, forms: [], + currentLocation: null, + locationIsGranted: false, }); export default UserState; From 4960b16f65e8195ef98591090fe40e8096ff0399 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Fri, 2 Feb 2024 19:47:23 +0700 Subject: [PATCH 02/22] [#1076] Add new buildParams state: gpsInterval --- app/src/store/buildParams.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/store/buildParams.js b/app/src/store/buildParams.js index 7449289cf..0a83b458a 100644 --- a/app/src/store/buildParams.js +++ b/app/src/store/buildParams.js @@ -13,7 +13,8 @@ const BuildParamsState = new Store({ errorHandling: defaultBuildParams?.errorHandling || true, loggingLevel: defaultBuildParams?.loggingLevel || 'verbose', appVersion: defaultBuildParams?.appVersion || '1.0.0', - gpsThreshold: defaultBuildParams?.gpsThreshold || 20, + gpsThreshold: defaultBuildParams?.gpsThreshold || 20, // meters + gpsInterval: 5, // seconds }); export default BuildParamsState; From 1dfa2fa6b0991c122f0151d5c90d28a69b9f8b66 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Fri, 2 Feb 2024 19:49:16 +0700 Subject: [PATCH 03/22] [#1076] Request the initial access location --- app/App.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/App.js b/app/App.js index 54c88b264..a29f9c207 100644 --- a/app/App.js +++ b/app/App.js @@ -18,6 +18,7 @@ import backgroundTask, { } from './src/lib/background-task'; import crudJobs, { jobStatus, MAX_ATTEMPT } from './src/database/crud/crud-jobs'; import { ToastAndroid } from 'react-native'; +import * as Location from 'expo-location'; export const setNotificationHandler = () => Notifications.setNotificationHandler({ @@ -85,6 +86,7 @@ TaskManager.defineTask(SYNC_FORM_SUBMISSION_TASK_NAME, async () => { const App = () => { const serverURLState = BuildParamsState.useState((s) => s.serverURL); const syncValue = BuildParamsState.useState((s) => s.dataSyncInterval); + const locationIsGranted = UserState.useState((s) => s.locationIsGranted); const handleCheckSession = () => { // check users exist @@ -187,6 +189,22 @@ const App = () => { handleOnRegisterTask(); }, [handleOnRegisterTask]); + const requestAccessLocation = useCallback(async () => { + if (locationIsGranted) { + return; + } + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status === 'granted') { + UserState.update((s) => { + s.locationIsGranted = true; + }); + } + }, [locationIsGranted]); + + useEffect(() => { + requestAccessLocation(); + }, [requestAccessLocation]); + return ( From 0c904ede1611fcceaea2021a700ea145e5a78f73 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Fri, 2 Feb 2024 19:50:04 +0700 Subject: [PATCH 04/22] [#1076] Subscribe to the user's current location in Homepage --- app/src/pages/Home.js | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/app/src/pages/Home.js b/app/src/pages/Home.js index 9c6fc0579..e7c193b48 100644 --- a/app/src/pages/Home.js +++ b/app/src/pages/Home.js @@ -3,10 +3,11 @@ import { Button } from '@rneui/themed'; import Icon from 'react-native-vector-icons/Ionicons'; import { Platform, ToastAndroid } from 'react-native'; import { BaseLayout } from '../components'; -import { FormState, UserState, UIState } from '../store'; +import { FormState, UserState, UIState, BuildParamsState } from '../store'; import { crudForms } from '../database/crud'; import { i18n } from '../lib'; import * as Notifications from 'expo-notifications'; +import * as Location from 'expo-location'; const Home = ({ navigation, route }) => { const params = route?.params || null; @@ -15,6 +16,9 @@ const Home = ({ navigation, route }) => { const [appLang, setAppLang] = useState('en'); const [loading, setloading] = useState(true); + const locationIsGranted = UserState.useState((s) => s.locationIsGranted); + const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold); + const gpsInterval = BuildParamsState.useState((s) => s.gpsInterval); const isManualSynced = UIState.useState((s) => s.isManualSynced); const activeLang = UIState.useState((s) => s.lang); const trans = i18n.text(activeLang); @@ -101,6 +105,34 @@ const Home = ({ navigation, route }) => { return () => subscription.remove(); }, []); + const watchCurrentPosition = useCallback(async () => { + if (!locationIsGranted) { + return; + } + const timeInterval = gpsInterval * 1000; // miliseconds + const watch = await Location.watchPositionAsync( + { + accuracy: gpsThreshold, + timeInterval, + }, + (res) => { + UserState.update((s) => { + s.currentLocation = res; + }); + }, + ); + return watch; + }, [gpsThreshold, gpsInterval, locationIsGranted]); + + useEffect(() => { + /** + * Subscribe to the user's current location + * @tutorial https://docs.expo.dev/versions/latest/sdk/location/#locationwatchpositionasyncoptions-callback + */ + const watch = watchCurrentPosition(); + return () => watch.remove(); + }, [watchCurrentPosition]); + return ( Date: Fri, 2 Feb 2024 19:52:17 +0700 Subject: [PATCH 05/22] [#1076] Insert a saved location when a GEO question has no answer after timeout --- app/src/form/fields/TypeGeo.js | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/app/src/form/fields/TypeGeo.js b/app/src/form/fields/TypeGeo.js index a07a74886..e30885cbb 100644 --- a/app/src/form/fields/TypeGeo.js +++ b/app/src/form/fields/TypeGeo.js @@ -2,27 +2,28 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { View } from 'react-native'; import { Text, Button } from '@rneui/themed'; -import { UIState, FormState, BuildParamsState } from '../../store'; +import { UIState, FormState, BuildParamsState, UserState } from '../../store'; import { FieldLabel } from '../support'; import { styles } from '../styles'; import { loc, i18n } from '../../lib'; +const GEO_TIMEOUT = 60; // seconds + const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) => { const [errorMsg, setErrorMsg] = useState(null); const [gpsAccuracy, setGpsAccuracy] = useState(null); - const [currLocation, setCurrLocation] = useState({ lat: null, lng: null }); const [loading, setLoading] = useState(false); const [latitude, longitude] = value || []; const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold); - const isOnline = UIState.useState((s) => s.online); const activeLang = FormState.useState((s) => s.lang); + const savedLocation = UserState.useState((s) => s.currentLocation); const trans = i18n.text(activeLang); const requiredValue = required ? requiredSign : null; - const handleGetCurrLocation = useCallback(async () => { + const getCurrentLocation = async () => { setLoading(true); await loc.getCurrentLocation( ({ coords }) => { @@ -32,10 +33,6 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) */ setGpsAccuracy(Math.floor(accuracy)); // console.info('GPS accuracy:', accuracy, 'GPS Threshold:', gpsThreshold); - setCurrLocation({ - lat, - lng, - }); FormState.update((s) => { s.currentValues = { ...s.currentValues, [id]: [lat, lng] }; }); @@ -45,13 +42,34 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) setLoading(false); setErrorMsg(message); setGpsAccuracy(-1); - setCurrLocation({ lat: -1.3855559, lng: 37.9938594 }); + FormState.update((s) => { s.currentValues = { ...s.currentValues, [id]: [-1.3855559, 37.9938594] }; }); }, ); - }, [gpsThreshold, id]); + }; + + const handleGetCurrLocation = async () => { + const geoTimeout = GEO_TIMEOUT * 1000; + setTimeout(() => { + if (!value?.length && savedLocation?.coords) { + /** + * Insert a saved location when a GEO question has no answer after timeout + */ + const { latitude: lat, longitude: lng, accuracy } = savedLocation.coords; + setGpsAccuracy(Math.floor(accuracy)); + FormState.update((s) => { + s.currentValues = { ...s.currentValues, [id]: [lat, lng] }; + }); + } + if (loading) { + setLoading(false); + } + }, geoTimeout); + + await getCurrentLocation(); + }; return ( From 0d8fa6c243cb38735c6f814ccb6fcd42cf6e4259 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:14:37 +0700 Subject: [PATCH 06/22] [#1076] Add geo params: GPS interval, accuracy level & geoloc timeout --- app/src/store/buildParams.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/store/buildParams.js b/app/src/store/buildParams.js index 0a83b458a..45c112f8a 100644 --- a/app/src/store/buildParams.js +++ b/app/src/store/buildParams.js @@ -14,7 +14,9 @@ const BuildParamsState = new Store({ loggingLevel: defaultBuildParams?.loggingLevel || 'verbose', appVersion: defaultBuildParams?.appVersion || '1.0.0', gpsThreshold: defaultBuildParams?.gpsThreshold || 20, // meters - gpsInterval: 5, // seconds + gpsInterval: 60, // seconds + gpsAccuracyLevel: 4, // High + geoLocationTimeout: 60, // seconds }); export default BuildParamsState; From 8be53fbe24c2497ea59bd82ffc353d5804f13acb Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:15:07 +0700 Subject: [PATCH 07/22] [#1076] Add accuracy levels list --- app/src/lib/loc.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/lib/loc.js b/app/src/lib/loc.js index dd45ed767..6d6b89eeb 100644 --- a/app/src/lib/loc.js +++ b/app/src/lib/loc.js @@ -19,3 +19,26 @@ const loc = { }; export default loc; + +export const accuracyLevels = [ + { + label: 'Lowest', + value: 1, + }, + { + label: 'Low', + value: 2, + }, + { + label: 'Balanced', + value: 3, + }, + { + label: 'High', + value: 4, + }, + { + label: 'Highest', + value: 5, + }, +]; From a022242dae2c474ee0833c8590fb3a7e3010df1d Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:15:45 +0700 Subject: [PATCH 08/22] [#1076] Get geolocation timeout from global state --- app/src/form/fields/TypeGeo.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/form/fields/TypeGeo.js b/app/src/form/fields/TypeGeo.js index e30885cbb..ca31ec32b 100644 --- a/app/src/form/fields/TypeGeo.js +++ b/app/src/form/fields/TypeGeo.js @@ -1,20 +1,19 @@ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState } from 'react'; import { View } from 'react-native'; import { Text, Button } from '@rneui/themed'; -import { UIState, FormState, BuildParamsState, UserState } from '../../store'; +import { FormState, BuildParamsState, UserState } from '../../store'; import { FieldLabel } from '../support'; import { styles } from '../styles'; import { loc, i18n } from '../../lib'; -const GEO_TIMEOUT = 60; // seconds - const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) => { const [errorMsg, setErrorMsg] = useState(null); const [gpsAccuracy, setGpsAccuracy] = useState(null); const [loading, setLoading] = useState(false); const [latitude, longitude] = value || []; + const geoLocationTimeout = BuildParamsState.useState((s) => s.geoLocationTimeout); const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold); const activeLang = FormState.useState((s) => s.lang); const savedLocation = UserState.useState((s) => s.currentLocation); @@ -51,7 +50,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) }; const handleGetCurrLocation = async () => { - const geoTimeout = GEO_TIMEOUT * 1000; + const geoTimeout = geoLocationTimeout * 1000; setTimeout(() => { if (!value?.length && savedLocation?.coords) { /** From c2e782972b4b14ef8332a8e78ce93b0934af2e5f Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:16:19 +0700 Subject: [PATCH 09/22] [#1076] Add new config columns for Geolocation settings --- app/src/database/tables.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/database/tables.js b/app/src/database/tables.js index 24c116e81..99158ddfe 100644 --- a/app/src/database/tables.js +++ b/app/src/database/tables.js @@ -20,6 +20,9 @@ export const tables = [ syncInterval: 'REAL', syncWifiOnly: 'TINYINT', lang: 'VARCHAR(255) DEFAULT "en" NOT NULL', + gpsThreshold: 'INTEGER NULL', + gpsAccuracyLevel: 'INTEGER NULL', + geoLocationTimeout: 'INTEGER NULL', }, }, { From f0c330ede154c1ab121224cf1fa7759942e746db Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:17:03 +0700 Subject: [PATCH 10/22] [#1076] Update settings config to add: Geolocation section --- app/src/pages/Settings/config.js | 97 +++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/app/src/pages/Settings/config.js b/app/src/pages/Settings/config.js index 45854054e..636e27298 100644 --- a/app/src/pages/Settings/config.js +++ b/app/src/pages/Settings/config.js @@ -1,3 +1,5 @@ +import { accuracyLevels } from '../../lib/loc'; + export const config = [ { id: 1, @@ -51,7 +53,7 @@ export const config = [ { id: 31, type: 'number', - name: 'syncInterval', + name: 'dataSyncInterval', label: 'Sync interval', description: { name: 'Sync interval in seconds', @@ -62,7 +64,7 @@ export const config = [ }, ], }, - key: 'UserState.syncInterval', + key: 'BuildParamsState.dataSyncInterval', editable: true, translations: [ { @@ -96,6 +98,97 @@ export const config = [ }, ], }, + { + id: 2, + name: 'Geolocation', + translations: [ + { + language: 'fr', + name: 'Géolocalisation', + }, + ], + description: { + name: 'GPS threshold, Accuracy Level, Geolocation timeout', + translations: [ + { + language: 'fr', + name: "Seuil GPS, Niveau de précision, Délai d'expiration de géolocalisation", + }, + ], + }, + fields: [ + { + id: 41, + type: 'number', + name: 'gpsThreshold', + label: 'GPS threshold', + description: { + name: 'GPS threshold in meters', + translations: [ + { + language: 'fr', + name: 'Seuil GPS en mètres', + }, + ], + }, + key: 'BuildParamsState.gpsThreshold', + editable: true, + translations: [ + { + language: 'fr', + name: 'Seuil GPS', + }, + ], + }, + { + id: 42, + type: 'dropdown', + name: 'gpsAccuracyLevel', + label: 'Accuracy level', + description: { + name: 'The level of location manager accuracy', + translations: [ + { + language: 'fr', + name: 'Le niveau de précision du gestionnaire de localisation.', + }, + ], + }, + key: 'BuildParamsState.gpsAccuracyLevel', + editable: true, + translations: [ + { + language: 'fr', + name: 'Niveau de précision', + }, + ], + options: accuracyLevels, + }, + { + id: 43, + type: 'number', + name: 'geoLocationTimeout', + label: 'Geolocation Timeout', + description: { + name: 'Timeout for taking points on geolocation questions in seconds', + translations: [ + { + language: 'fr', + name: "Délai d'expiration pour prendre des points sur les questions de géolocalisation en secondes", + }, + ], + }, + key: 'BuildParamsState.geoLocationTimeout', + editable: true, + translations: [ + { + language: 'fr', + name: "Délai d'expiration de la géolocalisation", + }, + ], + }, + ], + }, ]; export const langConfig = { From 0232f16e72e0ae861ee0b9d03868667246102803 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:18:45 +0700 Subject: [PATCH 11/22] [#1076] Implement geolocation settings --- app/src/pages/Settings/SettingsForm.js | 124 +++++++++++++------------ 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/app/src/pages/Settings/SettingsForm.js b/app/src/pages/Settings/SettingsForm.js index ec0228260..b575304b5 100644 --- a/app/src/pages/Settings/SettingsForm.js +++ b/app/src/pages/Settings/SettingsForm.js @@ -1,13 +1,15 @@ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { View } from 'react-native'; import { ListItem, Switch } from '@rneui/themed'; import * as Crypto from 'expo-crypto'; + import { BaseLayout } from '../../components'; import { config } from './config'; import { BuildParamsState, UIState, AuthState, UserState } from '../../store'; import { conn, query } from '../../database'; import DialogForm from './DialogForm'; import { backgroundTask, i18n } from '../../lib'; +import { accuracyLevels } from '../../lib/loc'; const db = conn.init; @@ -16,11 +18,8 @@ const SettingsForm = ({ route }) => { const [list, setList] = useState([]); const [showDialog, setShowDialog] = useState(false); - const { - serverURL, - appVersion, - dataSyncInterval: syncInterval, - } = BuildParamsState.useState((s) => s); + const { serverURL, dataSyncInterval, gpsThreshold, gpsAccuracyLevel, geoLocationTimeout } = + BuildParamsState.useState((s) => s); const { password, authenticationCode, useAuthenticationCode } = AuthState.useState((s) => s); const { lang, isDarkMode, fontSize } = UIState.useState((s) => s); const { name, syncWifiOnly } = UserState.useState((s) => s); @@ -39,8 +38,11 @@ const SettingsForm = ({ route }) => { lang, isDarkMode, fontSize, - syncInterval, + dataSyncInterval, syncWifiOnly, + gpsThreshold, + gpsAccuracyLevel, + geoLocationTimeout, }); const nonEnglish = lang !== 'en'; @@ -74,23 +76,15 @@ const SettingsForm = ({ route }) => { 'syncInterval', 'syncWifiOnly', 'lang', + 'gpsThreshold', + 'gpsAccuracyLevel', + 'geoLocationTimeout', ]; const id = 1; if (configFields.includes(field)) { const updateQuery = query.update('config', { id }, { [field]: value }); await conn.tx(db, updateQuery, [id]); } - if (configFields.includes('syncInterval')) { - try { - await backgroundTask.unregisterBackgroundTask('sync-form-submission'); - await backgroundTask.registerBackgroundTask('sync-form-submission', parseInt(value)); - } catch (error) { - console.error('[ERROR RESTART TASK]', error); - } - BuildParamsState.update((s) => { - s.dataSyncInterval = value; - }); - } if (field === 'name') { const updateQuery = query.update('users', { id }, { name: value }); await conn.tx(db, updateQuery, [id]); @@ -102,7 +96,16 @@ const SettingsForm = ({ route }) => { } }; - const handleOKPress = (inputValue) => { + const handleOnRestarTask = async () => { + try { + await backgroundTask.unregisterBackgroundTask('sync-form-submission'); + await backgroundTask.registerBackgroundTask('sync-form-submission', parseInt(value)); + } catch (error) { + console.error('[ERROR RESTART TASK]', error); + } + }; + + const handleOKPress = async (inputValue) => { setShowDialog(false); if (edit && inputValue) { const [stateData, stateKey] = editState; @@ -113,7 +116,12 @@ const SettingsForm = ({ route }) => { ...settingsState, [stateKey]: inputValue, }); - handleUpdateOnDB(stateKey, inputValue); + if (stateKey === 'dataSyncInterval') { + await handleUpdateOnDB('syncInterval', inputValue); + await handleOnRestarTask(); + } else { + await handleUpdateOnDB(stateKey, inputValue); + } setEdit(null); } }; @@ -135,44 +143,46 @@ const SettingsForm = ({ route }) => { handleUpdateOnDB(stateKey, tinyIntVal); }; - const handleCreateNewConfig = () => { - const insertQuery = query.insert('config', { - id: 1, - appVersion, - authenticationCode: 'testing', - serverURL, - syncInterval, - syncWifiOnly, - lang, - }); - conn.tx(db, insertQuery, []); + const renderSubtitle = ({ type: inputType, name, description }) => { + const itemDesc = nonEnglish ? i18n.transform(lang, description)?.name : description?.name; + if (inputType === 'switch' || inputType === 'password') { + return itemDesc; + } + if (name === 'gpsAccuracyLevel' && settingsState?.[name]) { + const findLevel = accuracyLevels.find((l) => l.value === settingsState[name]); + return findLevel?.label || itemDesc; + } + return settingsState?.[name]; }; - const settingsID = useMemo(() => { - return route?.params?.id; - }, [route]); + const loadSettings = useCallback(async () => { + const selectQuery = query.read('config', { id: 1 }); + const { rows } = await conn.tx(db, selectQuery, [1]); + + const configRows = rows._array[0]; + setSettingsState({ + ...settingsState, + ...configRows, + dataSyncInterval: configRows?.syncInterval || dataSyncInterval, + }); + }, []); + + const loadSettingsItems = useCallback(() => { + if (route.params?.id) { + const findConfig = config.find((c) => c?.id === route.params.id); + const fields = findConfig ? findConfig.fields : []; + setList(fields); + } + }, [route.params?.id]); useEffect(() => { - const findConfig = config.find((c) => c?.id === settingsID); - const fields = findConfig ? findConfig.fields : []; - setList(fields); - }, [settingsID]); + loadSettingsItems(); + }, [loadSettingsItems]); useEffect(() => { - const selectQuery = query.read('config', { id: 1 }); - conn.tx(db, selectQuery, [1]).then(({ rows }) => { - if (rows.length) { - const configRows = rows._array[0]; - setSettingsState({ - ...settingsState, - ...configRows, - syncInterval, - }); - } else { - handleCreateNewConfig(); - } - }); - }, []); + loadSettings(); + }, [loadSettings]); + return ( @@ -184,18 +194,11 @@ const SettingsForm = ({ route }) => { l.editable && l.type !== 'switch' ? { onPress: () => handleEditPress(l.id) } : {}; const itemTitle = nonEnglish ? i18n.transform(lang, l)?.label : l.label; - const itemDesc = nonEnglish - ? i18n.transform(lang, l?.description)?.name - : l?.description?.name; - const subtitle = - l.type === 'switch' || l.type === 'password' - ? itemDesc - : settingsState[l.name] || itemDesc; return ( {itemTitle} - {subtitle} + {renderSubtitle(l)} {l.type === 'switch' && ( { onCancel={handleCancelPress} showDialog={showDialog} edit={edit} + initValue={edit?.value} /> From 804954abf30cb466c3adc1fea383fafadc27979c Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:19:19 +0700 Subject: [PATCH 12/22] [#1076] Fix keyboard type for number field & show description --- app/src/pages/Settings/DialogForm.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/pages/Settings/DialogForm.js b/app/src/pages/Settings/DialogForm.js index 89683665d..a8379f5fd 100644 --- a/app/src/pages/Settings/DialogForm.js +++ b/app/src/pages/Settings/DialogForm.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Dialog, Input, Slider } from '@rneui/themed'; +import { Dialog, Input, Slider, Text } from '@rneui/themed'; import { Dropdown } from 'react-native-element-dropdown'; import Icon from 'react-native-vector-icons/Ionicons'; import { UIState } from '../../store'; @@ -10,7 +10,7 @@ const DialogForm = ({ onOk, onCancel, showDialog, edit, initValue = 0 }) => { const activeLang = UIState.useState((s) => s.lang); const trans = i18n.text(activeLang); - const { type, label, slider, value: defaultValue, options } = edit || {}; + const { type, label, slider, value: defaultValue, options, description } = edit || {}; const isPassword = type === 'password' || false; return ( @@ -35,6 +35,7 @@ const DialogForm = ({ onOk, onCancel, showDialog, edit, initValue = 0 }) => { onChangeText={setValue} defaultValue={defaultValue?.toString()} testID="settings-form-input" + keyboardType={type === 'number' ? 'number-pad' : 'default'} /> )} {type === 'dropdown' && ( @@ -51,6 +52,7 @@ const DialogForm = ({ onOk, onCancel, showDialog, edit, initValue = 0 }) => { testID="settings-form-dropdown" /> )} + {description?.name && {i18n.transform(activeLang, description)?.name}} onOk(value)} testID="settings-form-dialog-ok"> {trans.buttonOk} From 94c011946293d96c0d808f751160a8d119524022 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:20:39 +0700 Subject: [PATCH 13/22] [#1076] Fix bug watchPositionAsync unsubscribe calling --- app/src/pages/Home.js | 59 +++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/app/src/pages/Home.js b/app/src/pages/Home.js index a1db3aed6..493150ac5 100644 --- a/app/src/pages/Home.js +++ b/app/src/pages/Home.js @@ -17,7 +17,7 @@ const Home = ({ navigation, route }) => { const [loading, setloading] = useState(true); const locationIsGranted = UserState.useState((s) => s.locationIsGranted); - const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold); + const gpsAccuracyLevel = BuildParamsState.useState((s) => s.gpsAccuracyLevel); const gpsInterval = BuildParamsState.useState((s) => s.gpsInterval); const isManualSynced = UIState.useState((s) => s.isManualSynced); const activeLang = UIState.useState((s) => s.lang); @@ -105,32 +105,41 @@ const Home = ({ navigation, route }) => { return () => subscription.remove(); }, []); - const watchCurrentPosition = useCallback(async () => { - if (!locationIsGranted) { - return; - } - const timeInterval = gpsInterval * 1000; // miliseconds - const watch = await Location.watchPositionAsync( - { - accuracy: gpsThreshold, - timeInterval, - }, - (res) => { - UserState.update((s) => { - s.currentLocation = res; - }); - }, - ); - return watch; - }, [gpsThreshold, gpsInterval, locationIsGranted]); + const watchCurrentPosition = useCallback( + async (unsubscribe = false) => { + if (!locationIsGranted) { + return; + } + const timeInterval = gpsInterval * 1000; // miliseconds + /** + * Subscribe to the user's current location + * @tutorial https://docs.expo.dev/versions/latest/sdk/location/#locationwatchpositionasyncoptions-callback + */ + const watch = await Location.watchPositionAsync( + { + accuracy: gpsAccuracyLevel, + timeInterval, + }, + (res) => { + console.info('[CURRENT LOC]', res?.coords); + UserState.update((s) => { + s.currentLocation = res; + }); + }, + ); + + if (unsubscribe) { + watch.remove(); + } + }, + [gpsAccuracyLevel, gpsInterval, locationIsGranted], + ); useEffect(() => { - /** - * Subscribe to the user's current location - * @tutorial https://docs.expo.dev/versions/latest/sdk/location/#locationwatchpositionasyncoptions-callback - */ - const watch = watchCurrentPosition(); - return () => watch.remove(); + watchCurrentPosition(); + return () => { + watchCurrentPosition(true); + }; }, [watchCurrentPosition]); return ( From 96d7de861d802853757bdafa0c0afd33bc17827e Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:21:30 +0700 Subject: [PATCH 14/22] [#1076] Remove unnecessary update config db in initConfig --- app/App.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/App.js b/app/App.js index a29f9c207..233869d52 100644 --- a/app/App.js +++ b/app/App.js @@ -86,6 +86,9 @@ TaskManager.defineTask(SYNC_FORM_SUBMISSION_TASK_NAME, async () => { const App = () => { const serverURLState = BuildParamsState.useState((s) => s.serverURL); const syncValue = BuildParamsState.useState((s) => s.dataSyncInterval); + const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold); + const gpsAccuracyLevel = BuildParamsState.useState((s) => s.gpsAccuracyLevel); + const geoLocationTimeout = BuildParamsState.useState((s) => s.geoLocationTimeout); const locationIsGranted = UserState.useState((s) => s.locationIsGranted); const handleCheckSession = () => { @@ -120,11 +123,12 @@ const App = () => { const serverURL = configExist?.serverURL || serverURLState; const syncInterval = configExist?.syncInterval || syncValue; if (!configExist) { - await crudConfig.addConfig({ serverURL }); - } - if (syncInterval) { - BuildParamsState.update((s) => { - s.dataSyncInterval = syncInterval; + await crudConfig.addConfig({ + serverURL, + syncInterval, + gpsThreshold, + gpsAccuracyLevel, + geoLocationTimeout, }); } if (serverURL) { @@ -132,14 +136,17 @@ const App = () => { s.serverURL = serverURL; }); api.setServerURL(serverURL); - await crudConfig.updateConfig({ serverURL }); } console.info('[CONFIG] Server URL', serverURL); }; const handleInitDB = useCallback(async () => { + /** + * Exclude the reset in the try-catch block + * to prevent other queries from being skipped after this process. + */ + await conn.reset(); try { - await conn.reset(); const db = conn.init; const queries = tables.map((t) => { const queryString = query.initialQuery(t.name, t.fields); From bcf82469182c4f349a5accd5fcff1554f2938947 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:56:33 +0700 Subject: [PATCH 15/22] [#1076] Update global state settings with values from database --- app/App.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/App.js b/app/App.js index 233869d52..194275b3b 100644 --- a/app/App.js +++ b/app/App.js @@ -137,6 +137,17 @@ const App = () => { }); api.setServerURL(serverURL); } + if (configExist) { + /** + * Update settings values from database + */ + BuildParamsState.update((s) => { + s.dataSyncInterval = configExist.syncInterval; + s.gpsThreshold = configExist.gpsThreshold; + s.gpsAccuracyLevel = configExist.gpsAccuracyLevel; + s.geoLocationTimeout = configExist.geoLocationTimeout; + }); + } console.info('[CONFIG] Server URL', serverURL); }; From e7a5e9262fa073678be458988b9f4616d4257dd1 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 16:57:16 +0700 Subject: [PATCH 16/22] [#1076] Implement geolocation accuracy level from settings in TypeGeo --- app/src/form/fields/TypeGeo.js | 2 ++ app/src/lib/loc.js | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/form/fields/TypeGeo.js b/app/src/form/fields/TypeGeo.js index ca31ec32b..d94acae9e 100644 --- a/app/src/form/fields/TypeGeo.js +++ b/app/src/form/fields/TypeGeo.js @@ -13,6 +13,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) const [loading, setLoading] = useState(false); const [latitude, longitude] = value || []; + const gpsAccuracyLevel = BuildParamsState.useState((s) => s.gpsAccuracyLevel); const geoLocationTimeout = BuildParamsState.useState((s) => s.geoLocationTimeout); const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold); const activeLang = FormState.useState((s) => s.lang); @@ -46,6 +47,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) s.currentValues = { ...s.currentValues, [id]: [-1.3855559, 37.9938594] }; }); }, + gpsAccuracyLevel, ); }; diff --git a/app/src/lib/loc.js b/app/src/lib/loc.js index 6d6b89eeb..093ecb369 100644 --- a/app/src/lib/loc.js +++ b/app/src/lib/loc.js @@ -1,10 +1,12 @@ import * as Location from 'expo-location'; -const getCurrentLocation = async (success, error) => { +const getCurrentLocation = async (success, error, level = null) => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status === 'granted') { + const findLevel = accuracyLevels.find((l) => l.value === level); + const accuracy = findLevel?.value || Location.Accuracy.Highest; const result = await Location.getCurrentPositionAsync({ - accuracy: Location.Accuracy.Highest, + accuracy, }); success(result); } else { From 78326de4c76a934cde4a61e6e2521c7ad049c49a Mon Sep 17 00:00:00 2001 From: dedenbangkit Date: Mon, 5 Feb 2024 17:38:03 +0700 Subject: [PATCH 17/22] Change build badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e027d4cb6..8d1a5ee96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RTMIS -[![Build Status](https://akvo.semaphoreci.com/badges/rtmis/branches/main.svg?style=shields)](https://akvo.semaphoreci.com/projects/rtmis) [![Repo Size](https://img.shields.io/github/repo-size/akvo/rtmis)](https://img.shields.io/github/repo-size/akvo/rtmis) [![Languages](https://img.shields.io/github/languages/count/akvo/rtmis)](https://img.shields.io/github/languages/count/akvo/rtmis) [![Issues](https://img.shields.io/github/issues/akvo/rtmis)](https://img.shields.io/github/issues/akvo/rtmis) [![Last Commit](https://img.shields.io/github/last-commit/akvo/rtmis/main)](https://img.shields.io/github/last-commit/akvo/rtmis/main) [![Coverage Status](https://coveralls.io/repos/github/akvo/rtmis/badge.svg)](https://coveralls.io/github/akvo/rtmis) [![Coverage Status](https://img.shields.io/readthedocs/rtmis?label=read%20the%20docs)](https://rtmis.readthedocs.io/en/latest) +[![Build Status](https://github.com/akvo/rtmis/actions/workflows/main.yml/badge.svg)](https://github.com/akvo/rtmis/actions/workflows/main.yml?query=branch%3Amain) [![Build Status](https://github.com/akvo/rtmis/actions/workflows/apk-release.yml/badge.svg)](https://github.com/akvo/rtmis/actions/workflows/apk-release.yml?query=branch%3Amain) [![Repo Size](https://img.shields.io/github/repo-size/akvo/rtmis)](https://img.shields.io/github/repo-size/akvo/rtmis) [![Languages](https://img.shields.io/github/languages/count/akvo/rtmis)](https://img.shields.io/github/languages/count/akvo/rtmis) [![Issues](https://img.shields.io/github/issues/akvo/rtmis)](https://img.shields.io/github/issues/akvo/rtmis) [![Last Commit](https://img.shields.io/github/last-commit/akvo/rtmis/main)](https://img.shields.io/github/last-commit/akvo/rtmis/main) [![Coverage Status](https://coveralls.io/repos/github/akvo/rtmis/badge.svg)](https://coveralls.io/github/akvo/rtmis) [![Coverage Status](https://img.shields.io/readthedocs/rtmis?label=read%20the%20docs)](https://rtmis.readthedocs.io/en/latest) Real Time Monitoring Information Systems From 6185466a2f90e5b2a4c88f06cf04a5f49b093300 Mon Sep 17 00:00:00 2001 From: ifirmawan Date: Mon, 5 Feb 2024 18:43:22 +0700 Subject: [PATCH 18/22] [#1131] Fix saved answers are reset by prefilled Reset by prefilled when questions are hidden and shown back and forth. --- app/src/form/components/Question.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/form/components/Question.js b/app/src/form/components/Question.js index 961fbbaa9..7de4caadd 100644 --- a/app/src/form/components/Question.js +++ b/app/src/form/components/Question.js @@ -78,7 +78,17 @@ const Question = memo(({ group, activeQuestions = [], index }) => { }); FormState.update((s) => { const preValues = preFilled?.fill?.reduce((prev, current) => { - return { [current['id']]: current['answer'], ...prev }; + /** + * Make sure the answer criteria are not replaced by previous values + * eg: + * Previous value = "Update" + * Answer criteria = "New" + */ + const answer = + id === current['id'] + ? current['answer'] + : values?.[current['id']] || current['answer']; + return { [current['id']]: answer, ...prev }; }, {}); s.prefilled = preValues; }); From f7b40102d8f284003310b5d1a706ecae552b4d5c Mon Sep 17 00:00:00 2001 From: dedenbangkit Date: Tue, 6 Feb 2024 12:46:00 +0700 Subject: [PATCH 19/22] TODO for monitoring revision --- app/src/database/crud/crud-jobs.js | 2 +- app/src/database/tables.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/database/crud/crud-jobs.js b/app/src/database/crud/crud-jobs.js index e7e781c0e..a24e18187 100644 --- a/app/src/database/crud/crud-jobs.js +++ b/app/src/database/crud/crud-jobs.js @@ -44,7 +44,7 @@ const jobsQuery = () => { const insertQuery = query.insert(tableName, { ...data, createdAt, - uuid: Crypto.randomUUID(), + uuid: Crypto.randomUUID(), // TODO: Remove if not needed }); return await conn.tx(db, insertQuery, []); } catch (error) { diff --git a/app/src/database/tables.js b/app/src/database/tables.js index 24c116e81..b496b1d52 100644 --- a/app/src/database/tables.js +++ b/app/src/database/tables.js @@ -7,7 +7,7 @@ export const tables = [ password: 'TEXT', active: 'TINYINT', token: 'TEXT', - administrationList: 'TEXT', + administrationList: 'TEXT', // TODO: Remove }, }, { @@ -59,7 +59,7 @@ export const tables = [ formId: 'INTEGER NOT NULL', uuid: 'TEXT type UNIQUE', name: 'VARCHAR(255)', - administration: 'VARCHAR(255)', + administration: 'VARCHAR(255)', // TODO: Remove syncedAt: 'DATETIME', json: 'TEXT', }, @@ -76,7 +76,7 @@ export const tables = [ name: 'jobs', fields: { id: 'INTEGER PRIMARY KEY NOT NULL', - uuid: 'TEXT type UNIQUE', + uuid: 'TEXT type UNIQUE', // TODO: Remove if not used user: 'INTEGER NOT NULL', type: 'VARCHAR(191)', status: 'INTEGER NOT NULL', From f8052977374112af16301724de7ba35df36d2687 Mon Sep 17 00:00:00 2001 From: dedenbangkit Date: Tue, 6 Feb 2024 13:35:30 +0700 Subject: [PATCH 20/22] [#1076] Modify loading for GPS --- app/src/form/fields/TypeGeo.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/form/fields/TypeGeo.js b/app/src/form/fields/TypeGeo.js index d94acae9e..93f9cb6cc 100644 --- a/app/src/form/fields/TypeGeo.js +++ b/app/src/form/fields/TypeGeo.js @@ -24,7 +24,6 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) const requiredValue = required ? requiredSign : null; const getCurrentLocation = async () => { - setLoading(true); await loc.getCurrentLocation( ({ coords }) => { const { latitude: lat, longitude: lng, accuracy } = coords; @@ -53,6 +52,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) const handleGetCurrLocation = async () => { const geoTimeout = geoLocationTimeout * 1000; + setLoading(true); setTimeout(() => { if (!value?.length && savedLocation?.coords) { /** @@ -68,7 +68,6 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) setLoading(false); } }, geoTimeout); - await getCurrentLocation(); }; @@ -104,11 +103,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) )} -