From 0897b477c521f69823c29f4168599b9ad10c9f98 Mon Sep 17 00:00:00 2001 From: Tyler Roach <63416413+troach-salesforce@users.noreply.github.com> Date: Mon, 13 Apr 2020 15:29:51 -0400 Subject: [PATCH 01/12] Configure Android to use singleTask launchMode (#501) --- android/app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ad16b43852..d4c5db2aaf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,7 +38,8 @@ android:screenOrientation="portrait" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize" - android:exported="true"> + android:exported="true" + android:launchMode="singleTask"> Date: Mon, 13 Apr 2020 23:50:45 +0200 Subject: [PATCH 02/12] Spanish Translation from Miquel Vila Porte (#491) --- app/locales/es.json | 99 ++++++++++++++++++++++++++++++++++++++++ app/locales/languages.js | 4 +- i18next-parser.config.js | 2 +- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 app/locales/es.json diff --git a/app/locales/es.json b/app/locales/es.json new file mode 100644 index 0000000000..e040207255 --- /dev/null +++ b/app/locales/es.json @@ -0,0 +1,99 @@ +{ + "label": { + "about_title":"Acerca de", + "authorities_add_button_label":"Añadir fuente de datos fiable", + "authorities_add_url":"Añadir organismo via web", + "authorities_desc":"Elige las autoridades sanitarias de tu zona para obtener datos sobre posibles contactos con el COVID. Selecciona un nombre de la lista global, o introduce la URL del sitio web de una autoridad sanitaria que haya usado Safe Paths.", + "authorities_input_placeholder":"Introduce la URL aquí", + "authorities_no_sources":"No hay fuentes de datos", + "authorities_removal_alert_cancel":"Cancelar", + "authorities_removal_alert_desc":"Quieres eliminar este organismo como fuente de datos?", + "authorities_removal_alert_proceed":"Adelante", + "authorities_removal_alert_title":"Eliminar organismo", + "authorities_title":"Fuentes de datos fiables", + "choose_provider_subtitle":"Para informarte de posibles contactos, debes suscribirte a una autoridad sanitaria. ", + "choose_provider_title":"Elegir autoridades sanitarias", + "commitment":"Compromiso", + "commitment_para":"Safe Paths registra de forma segura tu ubicación y encuentra posibles contactos con el COVID. Tus datos NUNCA salen de tu dispositivo sin tu consentimiento.", + "default_news_site_name":"Noticias de Safe Paths", + "event_history_subtitle":"Entiende tu exposición al COVID-19 según los datos publicados por las autoridades sanitarias.", + "event_history_title":"Historial de exposición", + "export_para_1":"Si has dado positivo en COVID-19, ayuda a las autoridades locales compartiendo tu historial de ubicaciones.", + "export_para_2":"Tu historial se envía como una simple lista de ubicaciones en el tiempo, sin revelar datos personales.", + "home_at_risk_header":"Puedes haberte expuesto al COVID", + "home_at_risk_subsubtext":"Esto no implica que te hayas infectado.", + "home_at_risk_subtext":"Según tu historial de ubicaciones, es posible que hayas estado en contacto o cerca de alguien con COVID-19.", + "home_enable_location":"Permitir acceso a tu ubicación", + "home_mayo_link_heading":"Más información del COVID-19", + "home_mayo_link_label":"de expertos de la Mayo Clinic", + "home_next_steps":"Más información", + "home_no_contact_header":"No ha habido contacto", + "home_no_contact_subtext":"Según los datos disponibles, no has estado cerca de nadie que haya dado positivo en COVID-19.", + "home_unknown_header":"Desconocido", + "home_unknown_subtext":"Si no permites a la app el acceso a tu ubicación no puedes saber si has estado en riesgo", + "import_step_1":"1. Inicia sesión en tu cuenta de Google y descarga tu historial de ubicaciones", + "import_step_2":"2. Después de la descarga, vuelve a abrir esta ventana. Tus datos se importarán de forma automática", + "import_title":"Importar historial", + "latest_news":"Últimas noticias", + "launch_done_header":"Ya estás listo", + "launch_done_subheader":"Preparados para comenzar. Recuerda que puedes cambiar tus preferencias más tarde.", + "launch_enable_location":"Permitir acceso", + "launch_enable_notif":"Permitir notificaciones", + "launch_finish_set_up":"Finalizar configuración", + "launch_get_started":"Iniciar", + "launch_location_access":"Acceder a tu ubicación", + "launch_location_header":"Para recordar donde has estado, tu teléfono móvil necesita registrar tu ubicación.", + "launch_location_subheader":"No te preocupes, estos datos nunca saldrán de tu móvil sin tu consentimiento expreso.", + "launch_next":"Siguiente", + "launch_notif_header":"Con las notificaciones, podrás saber si has estado cerca de alguien con COVID.", + "launch_notif_subheader":"No te molestaremos salvo para enviarte información sobre tu riesgo de exposición.", + "launch_notification_access":"No te molestaremos salvo para enviarte información sobre tu riesgo de exposición.", + "launch_screen1_header":"La vuelta a la normalidad comienza aquí.", + "launch_screen2_header":"Recibe notificaciones si has estado con alguien que después dé positivo en COVID-19.", + "launch_screen2_subheader":"La información es poder.", + "launch_screen3_header":"Si das positivo, puedes optar por compartir tus datos de forma anónima", + "launch_screen3_subheader":"Ayuda a proteger a tu entorno más cercano.", + "launch_screen4_header":"Compartir depende de ti. Tus datos solo se guardan en tu móvil.", + "launch_screen4_subheader":"Si has dado positivo, solo tú eliges si quieres compartirlos.", + "launch_set_up_phone":"Configurar mi dispositivo", + "legal_page_title":"Aviso legal", + "less_than_one_minute":"menos de 1 minuto", + "loading_public_data":"cargando datos…", + "location_disabled_message":"COVID Safe Paths necesita tu ubicación.", + "location_disabled_title":"Registro de ubicación desactivado", + "location_enabled_message":"COVID Safe Paths está registrando en tu dispositivo tus coordenadas GPS cada 5 minutos de forma segura.", + "location_enabled_title":"COVID Safe Paths habilitado", + "maps_import_button_text":"Importar ubicaciones", + "maps_import_disclaimer":"Safe Paths no está afiliado con Google y nunca compartirá tus datos.", + "maps_import_text":"Para saber si has estado con alguien con COVID-19 antes de descargar esta app, puedes importar tu historial personal de ubicaciones.", + "maps_import_title":"Google Maps", + "nCoV2019_url_info":"Más información sobre los datos de este mapa", + "news_subtitle":"Novedades sobre el COVID-19 de tus autoridades sanitarias y noticias en general.", + "news_title":"Últimas Noticias", + "no_data":"No hay datos", + "notification_2_weeks_ago":"hace 2 semanas", + "notification_data_not_available":"No hay datos sobre exposición.", + "notification_random_data_button":"Elegir autoridades sanitarias", + "notification_title":"Perfil de exposición - 2 semanas", + "notification_today":"hoy", + "notification_warning_text":"Suscríbete a tus autoridades sanitarias para recibir datos sobre posibles riesgos de forma regular.", + "notifications_exposure_format":"Hace {{daysAgo}} días estuviste cerca de alguien con COVID-19 durante {{exposureTime}} minutos.", + "notifications_exposure_format_today":"Hoy has estado cerca de alguien con COVID-19 durante {{exposureTime}} minutos.", + "notifications_exposure_format_yesterday":"Ayer estuviste cerca de alguien con COVID-19 durante {{exposureTime}} minutos.", + "notifications_no_exposure":"No te has expuesto al COVID-19 en las últimas 2 semanas.", + "overlap_found_button_label":"Datos públicos descargados", + "overlap_no_results_button_label":"Datos públicos descargados", + "overlap_para_1":"El rastro verde representa tu historial de ubicaciones\\n\\nLos círculos violetas son los datos públicos", + "overlap_title":"Comprobar coincidencias", + "push_at_risk_message":"Has estado cerca de alguien con COVID-19", + "push_at_risk_title":"Puedes estar en riesgo", + "settings_title":"Panel de control", + "share_location_data":"Comparte tus datos", + "show_overlap":"Haz clic para ver información pública", + "team":"Equipo", + "team_para":"El equipo se compone de un consorcio de epidemiólogos, ingenieros, analistas de datos, defensores de la privacidad digital, profesores y investigadores de instituciones reconocidas como MIT, Harvard, la Mayo Clinic, TripleBlind, EyeNetra, Ernst & Young y Link Ventures.", + "terms_of_use":"Condiciones de uso", + "tested_positive_subtitle":"Puedes compartir tus datos con las autoridades sanitarias u otros agentes, o guardar una copia de seguridad", + "tested_positive_title":"Compartir ubicaciones" + } +} diff --git a/app/locales/languages.js b/app/locales/languages.js index 9a245b30ee..6127d6e919 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -4,6 +4,7 @@ import { getLanguages } from 'react-native-i18n'; import { LANG_OVERRIDE } from '../constants/storage'; import { GetStoreData } from '../helpers/General'; import en from './en.json'; +import es from './es.json'; import ht from './ht.json'; import it from './it.json'; @@ -56,8 +57,9 @@ i18next.init({ fallbackLng: 'en', // If language detector fails resources: { en: { label: 'English', translation: en }, + es: { label: 'Español', translation: es }, ht: { label: 'Kreyòl ayisyen', translation: ht }, - it: { label: 'Italian', translation: it }, + it: { label: 'Italiano', translation: it }, }, }); diff --git a/i18next-parser.config.js b/i18next-parser.config.js index d84b5ce044..296a0663e0 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -38,7 +38,7 @@ module.exports = { lineEnding: 'auto', // Control the line ending. See options at https://github.com/ryanve/eol - locales: ['en', 'ht', 'it'], + locales: ['en', 'es', 'ht', 'it'], // An array of the locales in your applications namespaceSeparator: ':', From 7880d17e5be7018893775aae1ea4139967081e16 Mon Sep 17 00:00:00 2001 From: Steve Penrod Date: Mon, 13 Apr 2020 17:03:04 -0500 Subject: [PATCH 03/12] Remove hardcoded link to Haitian health authority (#499) The News page had a hardcoded "Haitian Ministry of Health" entry. That has been removed. Also cleaned up a few other things: * Removed some console.log() messages * Added several variables to the storage.js list (if we are going to do that, we should do it everywhere). * Fixed the AUTHORITY_NEWS population (it was always serializing an empty list) * Add/Remove of a Healthcare Authority now kicks off an intersect calc immediately. This also updates the links to News, etc (but it can take a few seconds to complete in the background). --- app/constants/storage.js | 3 + app/helpers/Intersect.js | 58 +++++----- app/locales/en.json | 4 +- app/views/ChooseProvider.js | 27 +++-- app/views/News.js | 17 ++- .../__tests__/__snapshots__/News.spec.js.snap | 106 +----------------- 6 files changed, 63 insertions(+), 152 deletions(-) diff --git a/app/constants/storage.js b/app/constants/storage.js index 64133b259b..8ec1ed6709 100644 --- a/app/constants/storage.js +++ b/app/constants/storage.js @@ -4,3 +4,6 @@ export const PARTICIPATE = 'PARTICIPATE'; export const MY_UUIDs = 'MY_UUIDs'; export const CROSSED_PATHS = 'CROSSED_PATHS'; export const LANG_OVERRIDE = 'LANG_OVERRIDE'; +export const AUTHORITY_NEWS = 'AUTHORITY_NEWS'; +export const LAST_CHECKED = 'LAST_CHECKED'; +export const AUTHORITY_SOURCE_SETTINGS = 'AUTHORITY_SOURCE_SETTINGS'; diff --git a/app/helpers/Intersect.js b/app/helpers/Intersect.js index cb2f6bbc3b..604a99edb1 100644 --- a/app/helpers/Intersect.js +++ b/app/helpers/Intersect.js @@ -7,7 +7,13 @@ import PushNotification from 'react-native-push-notification'; import { isPlatformiOS } from './../Util'; -import { CROSSED_PATHS, LOCATION_DATA } from '../constants/storage'; +import { + LOCATION_DATA, + CROSSED_PATHS, + AUTHORITY_SOURCE_SETTINGS, + AUTHORITY_NEWS, + LAST_CHECKED, +} from '../constants/storage'; import { GetStoreData, SetStoreData } from '../helpers/General'; import languages from '../locales/languages'; @@ -214,22 +220,16 @@ export function checkIntersect() { // this.findNewAuthorities(); NOT IMPLEMENTED YET // Get the user's health authorities - GetStoreData('AUTHORITY_SOURCE_SETTINGS') + GetStoreData(AUTHORITY_SOURCE_SETTINGS) .then(authority_list => { if (!authority_list) { - // DEBUG: Force a test list - // authority_list = [ - // { - // name: 'Platte County Health', - // url: - // 'https://raw.githack.com/tripleblindmarket/safe-places/develop/examples/safe-paths.json', - // }, - //]; console.log('No authorities', authority_list); return; } let name_news = []; + SetStoreData(AUTHORITY_NEWS, name_news); + if (authority_list) { // Pull down data from all the registered health authorities authority_list = JSON.parse(authority_list); @@ -248,13 +248,24 @@ export function checkIntersect() { // { "time": 456, "latitude": 12.34, "longitude": 12.34} // ] // } - - // Update cache of info about the authority + // TODO: Add an "info_exposure_url" to allow recommendations for + // the health authority driectly on the Exposure History + // page (e.g. the "What Do I Do Now?" button) // TODO: Add an "info_newsflash" UTC timestamp and popup a // notification if that changes, i.e. if there is a newsflash? - name_news.push({ - name: responseJson.authority_name, - news_url: responseJson.info_website, + + // Update cache of info about the authority + GetStoreData(AUTHORITY_NEWS).then(nameNewsString => { + let name_news = []; + if (nameNewsString !== null) { + name_news = JSON.parse(nameNewsString); + } + + name_news.push({ + name: responseJson.authority_name, + news_url: responseJson.info_website, + }); + SetStoreData(AUTHORITY_NEWS, name_news); }); // TODO: Look at "publish_date_utc". We should notify users if @@ -270,20 +281,11 @@ export function checkIntersect() { }); }); - SetStoreData('AUTHORITY_NEWS', name_news) - .then(() => { - // TODO: Anything after this saves? Background caching of - // news to make it snappy? Could be a problem in some - // locales with high data costs. - }) - .catch(error => - console.log('Failed to save authority/news URL list', error), - ); + let nowUTC = new Date().toISOString(); + let unixtimeUTC = Date.parse(nowUTC); + // Last checked key is not being used atm. TODO check this to update periodically instead of every foreground activity + SetStoreData(LAST_CHECKED, unixtimeUTC); } - let nowUTC = new Date().toISOString(); - let unixtimeUTC = Date.parse(nowUTC); - // Last checked key is not being used atm. TODO check this to update periodically instead of every foreground activity - SetStoreData('LAST_CHECKED', unixtimeUTC); } else { console.log('No authority list'); return; diff --git a/app/locales/en.json b/app/locales/en.json index 249de4d19f..8896efe9ff 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -15,7 +15,7 @@ "choose_provider_title": "Choose health authority", "commitment": "Commitment", "commitment_para": "Safe Paths securely records and checks your interaction with people using your location. Your data will NEVER leave your phone without your consent.", - "default_news_site_name": "SafePaths News", + "default_news_site_name": "Safe Paths News", "default_news_site_url": "https://covidsafepaths.org/in-app-news", "event_history_subtitle": "Understand your personal exposure based on information shared by health authorities.", "event_history_title": "Exposure history", @@ -99,4 +99,4 @@ "tested_positive_subtitle": "Your private data can be transferred to health authorities, backed up, or otherwise shared.", "tested_positive_title": "Share location history" } -} +} \ No newline at end of file diff --git a/app/views/ChooseProvider.js b/app/views/ChooseProvider.js index 87a59bebd5..7206eaf77e 100644 --- a/app/views/ChooseProvider.js +++ b/app/views/ChooseProvider.js @@ -28,6 +28,8 @@ import closeIcon from './../assets/images/closeIcon.png'; import saveIcon from './../assets/images/saveIcon.png'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; import { AUTHORITIES_LIST_URL } from '../constants/authorities'; +import { AUTHORITY_SOURCE_SETTINGS } from '../constants/storage'; +import { checkIntersect } from '../helpers/Intersect'; import colors from '../constants/colors'; import Colors from '../constants/colors'; import fontFamily from '../constants/fonts'; @@ -64,7 +66,7 @@ class ChooseProviderScreen extends Component { this.fetchAuthoritiesList(); // Update user settings state from async storage - GetStoreData('AUTHORITY_SOURCE_SETTINGS', false).then(result => { + GetStoreData(AUTHORITY_SOURCE_SETTINGS, false).then(result => { if (result !== null) { console.log('Retrieving settings from async storage:'); console.log(result); @@ -103,7 +105,7 @@ class ChooseProviderScreen extends Component { : this.setState({ authoritiesList: [ { - 'Unable to load authorities list': [{ url: 'No URL' }], + 'Unable to load authorities list': [{ url: 'No URL' }], // TODO: Localize }, ], }); @@ -134,9 +136,12 @@ class ChooseProviderScreen extends Component { () => { // Add current settings state to async storage. SetStoreData( - 'AUTHORITY_SOURCE_SETTINGS', + AUTHORITY_SOURCE_SETTINGS, this.state.selectedAuthorities, - ); + ).then(() => { + // Force updates immediately. + checkIntersect(); + }); }, ); } else { @@ -164,9 +169,12 @@ class ChooseProviderScreen extends Component { () => { // Add current settings state to async storage. SetStoreData( - 'AUTHORITY_SOURCE_SETTINGS', + AUTHORITY_SOURCE_SETTINGS, this.state.selectedAuthorities, - ); + ).then(() => { + // Force updates immediately. + checkIntersect(); + }); }, ); } @@ -197,9 +205,12 @@ class ChooseProviderScreen extends Component { () => { // Add current settings state to async storage. SetStoreData( - 'AUTHORITY_SOURCE_SETTINGS', + AUTHORITY_SOURCE_SETTINGS, this.state.selectedAuthorities, - ); + ).then(() => { + // Force updates immediately. + checkIntersect(); + }); }, ); }, diff --git a/app/views/News.js b/app/views/News.js index bdc5982555..9a3a21d822 100644 --- a/app/views/News.js +++ b/app/views/News.js @@ -15,6 +15,7 @@ import languages from './../locales/languages'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; import colors from '../constants/colors'; import Colors from '../constants/colors'; +import { AUTHORITY_NEWS } from '../constants/storage'; // import { Colors } from 'react-native/Libraries/NewAppScreen'; import fontFamily from '../constants/fonts'; import { GetStoreData } from '../helpers/General'; @@ -80,23 +81,19 @@ class NewsScreen extends Component { componentDidMount() { BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); - GetStoreData('AUTHORITY_NEWS') - .then(name_news => { - console.log('name_news:', name_news); - + GetStoreData(AUTHORITY_NEWS) + .then(nameNewsString => { // Bring in news from the various authorities. This is // pulled down from the web when you subscribe to an Authority // on the Settings page. let arr = []; - // TODO: using this as test data for now without assigning - arr.push({ - name: 'Haitian Ministry of Health', - url: 'https://wmcelroy.wixsite.com/covidhaiti/kat', - }); + // Populate with subscribed news sources, with default at the tail + if (nameNewsString !== null) { + arr = JSON.parse(nameNewsString); + } arr.push(this.state.default_news); - console.log('name_news:', arr); this.setState({ newsUrls: arr, }); diff --git a/app/views/__tests__/__snapshots__/News.spec.js.snap b/app/views/__tests__/__snapshots__/News.spec.js.snap index 7b8dc94771..e88a2bb7bd 100644 --- a/app/views/__tests__/__snapshots__/News.spec.js.snap +++ b/app/views/__tests__/__snapshots__/News.spec.js.snap @@ -177,11 +177,7 @@ exports[`renders correctly 1`] = ` data={ Array [ Object { - "name": "Haitian Ministry of Health", - "url": "https://wmcelroy.wixsite.com/covidhaiti/kat", - }, - Object { - "name": "SafePaths News", + "name": "Safe Paths News", "url": "https://covidsafepaths.org/in-app-news", }, ] @@ -289,105 +285,7 @@ exports[`renders correctly 1`] = ` } } > - Haitian Ministry of Health - - - - - - - - - - - - - - SafePaths News + Safe Paths News Date: Mon, 13 Apr 2020 18:04:53 -0400 Subject: [PATCH 04/12] Android StatusBar transparency fix (#497) --- app/constants/colors.js | 1 + app/views/LocationTracking.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/constants/colors.js b/app/constants/colors.js index 54e05dcbc1..c105db7a8b 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -9,6 +9,7 @@ const colors = { PRIMARY_TEXT: '#000', GREEN: '#32A852', VIOLET: '#4051DB', + TRANSPARENT: 'transparent', // For notifications HIGHEST_RISK: '#e74c3c', diff --git a/app/views/LocationTracking.js b/app/views/LocationTracking.js index f4b5651deb..55ca967257 100644 --- a/app/views/LocationTracking.js +++ b/app/views/LocationTracking.js @@ -30,7 +30,7 @@ import SettingsGear from './../assets/svgs/settingsGear'; import StateAtRisk from './../assets/svgs/stateAtRisk'; import StateNoContact from './../assets/svgs/stateNoContact'; import StateUnknown from './../assets/svgs/stateUnknown'; -import { isPlatformiOS } from './../Util'; +import { isPlatformAndroid, isPlatformiOS } from './../Util'; import ButtonWrapper from '../components/ButtonWrapper'; import Colors from '../constants/colors'; import fontFamily from '../constants/fonts'; @@ -73,6 +73,12 @@ class LocationTracking extends Component { constructor(props) { super(props); + if (isPlatformAndroid()) { + StatusBar.setBackgroundColor(Colors.TRANSPARENT); + StatusBar.setBarStyle('light-content'); + StatusBar.setTranslucent(true); + } + this.state = { appState: AppState.currentState, timer_intersect: null, From 4d2468dc3a1d3b5488643a2e537f3a3edad593a9 Mon Sep 17 00:00:00 2001 From: alpita-masurkar Date: Tue, 14 Apr 2020 00:06:52 +0200 Subject: [PATCH 05/12] Italian i18n updates from @andreanuzzo, @diarmidmackenzie (#498) --- app/locales/it.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/locales/it.json b/app/locales/it.json index ddc79d2c7d..f00b3c3d26 100644 --- a/app/locales/it.json +++ b/app/locales/it.json @@ -3,7 +3,7 @@ "about_title": "Informazioni", "authorities_add_button_label": "Aggiungi Autorità", "authorities_add_url": "Aggiungi autorità via URL", - "authorities_desc": "Seleziona le autorità sanitarie nella tua area per ottenere i dati sulle possibilità di esposizione. Seleziona un nome dal registro globale o inserisci l'indirizzo internet fornito da un'autorità sanitaria che partecipa a Safe Paths", + "authorities_desc": "Seleziona le autorità sanitarie nella tua area per ottenere i dati sulle possibilità di esposizione.\nSeleziona un nome dal registro globale o inserisci l'indirizzo internet fornito da un'autorità sanitaria che partecipa a Safe Paths", "authorities_input_placeholder": "Incolla l'indirizzo qui", "authorities_no_sources": "Nessuna fonte di dati per ora", "authorities_removal_alert_cancel": "Annulla", @@ -64,7 +64,7 @@ "location_enabled_message": "COVID Safe Paths sta registrando in sicurezza le tue coordinate GPS ogni 5 minuti su questo dispositivo", "location_enabled_title": "COVID Safe Paths è abilitato", "maps_import_button_text": "Importa lo storico delle posizioni", - "maps_import_disclaimer": "Safe Paths non ha alcuna affiliazione a Google e non condivide mai I tuoi dati", + "maps_import_disclaimer": "Safe Paths non ha alcuna affiliazione a Google e non condivide mai i tuoi dati", "maps_import_text": "Per capire se hai incontrato qualcuno con COVID-19 prima di scaricare questa app, puoi importare lo storico delle tue posizioni", "maps_import_title": "Google Maps", "nCoV2019_url_info": "Per maggiori informazioni sui dati di questa mappa", @@ -73,7 +73,7 @@ "no_data": "Nessun dato", "notification_2_weeks_ago": "2 settimane precedenti", "notification_data_not_available": "Nessun dato disponibile su possibili esposizioni", - "notification_random_data_button": "Seleziona autorità sanitarie", + "notification_random_data_button": "Scegli le autorità sanitarie", "notification_title": "Profilo di esposizione nelle ultime 2 settimane", "notification_today": "Oggi", "notification_warning_text": "Se una autorità sanitaria esiste nella tua area, puoi registrarti per ottenere aggiornamenti sui rischi di possibili esposizioni", From 54bb332e9176035e0a1221f216345f60b832e11b Mon Sep 17 00:00:00 2001 From: haicker <61045676+ai-entrepreneur@users.noreply.github.com> Date: Tue, 14 Apr 2020 06:26:23 +0800 Subject: [PATCH 06/12] Feature/cn-language (#493) Co-authored-by: Ernie Co-authored-by: Tim Stirrat --- app/locales/languages.js | 4 +- app/locales/zh-Hant.json | 97 ++++++++++++++++++++++++++++++++++++++++ i18next-parser.config.js | 9 +++- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 app/locales/zh-Hant.json diff --git a/app/locales/languages.js b/app/locales/languages.js index 6127d6e919..616f901105 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -7,6 +7,7 @@ import en from './en.json'; import es from './es.json'; import ht from './ht.json'; import it from './it.json'; +import zh_Hant from './zh-Hant.json'; // Refer this for checking the codes and creating new folders https://developer.chrome.com/webstore/i18n @@ -59,7 +60,8 @@ i18next.init({ en: { label: 'English', translation: en }, es: { label: 'Español', translation: es }, ht: { label: 'Kreyòl ayisyen', translation: ht }, - it: { label: 'Italiano', translation: it }, + it: { label: 'Italian', translation: it }, + zh_Hant: { label: '繁體中文', translation: zh_Hant }, }, }); diff --git a/app/locales/zh-Hant.json b/app/locales/zh-Hant.json new file mode 100644 index 0000000000..f0ebee90fb --- /dev/null +++ b/app/locales/zh-Hant.json @@ -0,0 +1,97 @@ +{ + "label": { + "about_title": "關於", + "authorities_add_button_label": "新增可信來源", + "authorities_add_url": "新增可信的疾病管制機構網頁超連結", + "authorities_desc": "選擇您所在地區受信任的醫療機構,獲取其潛在感染高風險路徑數據。從全局註冊表(global registry)選取資料集名稱或從使用Safe Paths的官方網頁超連結", + "authorities_input_placeholder": "在此貼上網頁超連結", + "authorities_no_sources": "尚無資料來源", + "authorities_removal_alert_cancel": "取消", + "authorities_removal_alert_desc": "您確定要移除此疾病管制機構資料來源?", + "authorities_removal_alert_proceed": "繼續", + "authorities_removal_alert_title": "移除疾病管制官方機構", + "authorities_title": "可信來源", + "choose_provider_subtitle": "如要了解潛在接觸情況,您需要向疾病管制機構訂閱", + "choose_provider_title": "選擇疾病管制機構", + "commitment": "承諾", + "commitment_para": "Safe Paths安全的使用您位置資訊紀錄以及確認您與他人的互動,未經您允許,您的數據絕不會離開您手機", + "default_news_site_name": "SafePaths 新聞", + "event_history_subtitle": "從疾病管制機構共享的數據了解您個人接觸風險", + "event_history_title": "接觸歷史", + "export_para_1": "如果您新冠病毒檢測結果為陽性,請您盡己所能分享您的位置歷史資訊給在地的疾病管制機構", + "export_para_2": "位置資訊僅分享您所在位置與時間,並無其他多餘資訊", + "home_at_risk_header": "您可能有高接觸風險", + "home_at_risk_subsubtext": "這並不代表您被感染了", + "home_at_risk_subtext": "根據您的路徑歷史資訊,您有可能曾經與新冠病毒確診者或接觸者接觸", + "home_enable_location": "開啟位置數據", + "home_next_steps": "知道更多", + "home_no_contact_header": "無已知接觸", + "home_no_contact_subtext": "根據已知數據,您並未接觸任何新冠病毒確診者", + "home_unknown_header": "未知", + "home_unknown_subtext": "我們無法告知您是否曝露在接觸風險中,除非您允許此應用程式取得您位置資訊", + "import_step_1": "1. 登陸您Google帳號,並下載您的位置資訊", + "import_step_2": "2. 下載後,請再度打開此頁面,資料將自動導入", + "import_title": "輸入位置資訊", + "latest_news": "最新新聞", + "launch_done_header": "全部完成", + "launch_done_subheader": "您已經準備完成。請記得,您可以隨時更新您的偏好", + "launch_enable_location": "開啟位置資訊", + "launch_enable_notif": "開啟通知", + "launch_finish_set_up": "完成設置", + "launch_get_started": "開始", + "launch_location_access": "取得位置", + "launch_location_header": "為比對您與確診者的路徑,此應用程式需要取得您手機的位置資訊", + "launch_location_subheader": "別擔心,除非您確認要分享資訊,此資訊不會離開您手機", + "launch_next": "下一個", + "launch_notif_header": "如果您曾經與確診者路徑重疊,我們會推送通知讓您知曉", + "launch_notif_subheader": "除非通知您潛在的接觸風險,我們不會打擾您", + "launch_notification_access": "允許通知", + "launch_screen1_header": "安全之路,由此開始", + "launch_screen2_header": "立即得知您是否與新冠肺炎確診者重疊路徑", + "launch_screen2_subheader": "知識就是力量", + "launch_screen3_header": "若您確診了,您可以選擇匿名的分享您的資料", + "launch_screen3_subheader": "您可以幫助維護整個社區的安全", + "launch_screen4_header": "您有絕對的掌控權,資料只會存放在您的手機中", + "launch_screen4_subheader": "如果您確診了,您可以自主選擇是否分享資料", + "launch_set_up_phone": "設定我的手機", + "legal_page_title": "法遵", + "less_than_one_minute": "少於一分鐘", + "loading_public_data": "資料下載中...", + "location_disabled_message": "COVID Safe Paths 需要位置服務", + "location_disabled_title": "位置資訊已禁用", + "location_enabled_message": "COVID Safe Paths 每五分鐘在此裝置安全的儲存您的路徑資訊", + "location_enabled_title": "啟用 COVID Safe Paths", + "maps_import_button_text": "載入歷史路徑", + "maps_import_disclaimer": "Safe Paths與Google不存在從屬關係也不會分享您的資料", + "maps_import_text": "確認您下載此應用程式前,是否與新冠病毒確診者接觸,您可以載入您個人位置歷史資訊", + "maps_import_title": "Google 地圖", + "nCoV2019_url_info": "更多關於此地圖資料來源的資訊", + "news_subtitle": "從您在地與其他主要健康權威機構中,閱讀更多關於新冠病毒的最新資訊", + "news_title": "最新資訊", + "no_data": "沒有資料", + "notification_2_weeks_ago": "兩週以前", + "notification_data_not_available": "無可用的病毒接觸數據", + "notification_random_data_button": "選擇疾病管制機構", + "notification_title": "兩週內的接觸風險報告", + "notification_today": "今天", + "notification_warning_text": "若您所處地區的疾病管制機構存在,您可以訂閱並取得定期的接觸風險報告", + "notifications_exposure_format": "{{daysAgo}}天之前,您曾經與確診者重疊路徑{{exposureTime}}分鐘", + "notifications_exposure_format_today": "今天你與確診者重疊路徑{{exposureTime}}分鐘", + "notifications_exposure_format_yesterday": "昨天您與確診者重疊路徑{{exposureTime}}分鐘.", + "notifications_no_exposure": "過去兩星期內,沒有與新冠肺炎確診者接觸", + "overlap_found_button_label": "已下載公開數據", + "overlap_no_results_button_label": "已下載公開數據", + "overlap_para_1": "綠色路徑代表您的歷史路徑\n\n淡紫色圓環代表公開數據集", + "overlap_title": "確認路徑重疊", + "push_at_risk_message": "您與新冠肺炎確診者路徑重疊", + "push_at_risk_title": "您有可能曝露在接觸風險中", + "settings_title": "看板", + "share_location_data": "分享位置資訊", + "show_overlap": "點擊閱覽公開數據集", + "team": "團隊", + "team_para": "我們團隊是由一群來自知名機構的傳染病學家、工程師、資料科學家、數位隱私倡導者、教授以及研究員、包含:MIT, Harvard, The Mayo Clinic, TripleBlind, EyeNetra, Ernst & Young and Link Ventures.", + "terms_of_use": "使用條款", + "tested_positive_subtitle": "您的隱私資料可以被分享給疾病管制機構、背份、或者分享", + "tested_positive_title": "分享位置歷史資訊" + } +} diff --git a/i18next-parser.config.js b/i18next-parser.config.js index 296a0663e0..84d0a756e4 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -38,7 +38,14 @@ module.exports = { lineEnding: 'auto', // Control the line ending. See options at https://github.com/ryanve/eol - locales: ['en', 'es', 'ht', 'it'], + locales: [ + // sort alphabetically + 'en', + 'es', + 'ht', + 'it', + 'zh-Hant', + ], // An array of the locales in your applications namespaceSeparator: ':', From e8d2b15552ae04852c33215d0376a728146b28e4 Mon Sep 17 00:00:00 2001 From: alpita-masurkar Date: Tue, 14 Apr 2020 00:33:19 +0200 Subject: [PATCH 07/12] Russian Translation by Ksenia Lukacher (#479) --- app/locales/languages.js | 4 +- app/locales/ru.json | 99 ++++++++++++++++++++++++++++++++++++++++ i18next-parser.config.js | 1 + 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 app/locales/ru.json diff --git a/app/locales/languages.js b/app/locales/languages.js index 616f901105..36574e3bdd 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -7,6 +7,7 @@ import en from './en.json'; import es from './es.json'; import ht from './ht.json'; import it from './it.json'; +import ru from './ru.json'; import zh_Hant from './zh-Hant.json'; // Refer this for checking the codes and creating new folders https://developer.chrome.com/webstore/i18n @@ -60,7 +61,8 @@ i18next.init({ en: { label: 'English', translation: en }, es: { label: 'Español', translation: es }, ht: { label: 'Kreyòl ayisyen', translation: ht }, - it: { label: 'Italian', translation: it }, + it: { label: 'Italiano', translation: it }, + ru: { label: 'Русский', translation: ru }, zh_Hant: { label: '繁體中文', translation: zh_Hant }, }, }); diff --git a/app/locales/ru.json b/app/locales/ru.json new file mode 100644 index 0000000000..e864c6037b --- /dev/null +++ b/app/locales/ru.json @@ -0,0 +1,99 @@ +{ + "label": { + "about_title":"О нас", + "authorities_add_button_label":"Добавить надёжный источник", + "authorities_add_url":"Добавить организацию по ссылке", + "authorities_desc":"Выберите надёжные органы здравоохранения в вашем регионе, чтобы получать данные о зонах риска. Выберите название организации из международного реестра или введите ссылку организации, которая использует Safe Paths.", + "authorities_input_placeholder":"Вставьте ссылку сюда", + "authorities_no_sources":"Нет источника данных", + "authorities_removal_alert_cancel":"Отмена", + "authorities_removal_alert_desc":"Вы уверены, что хотите удалить этот источник данных?", + "authorities_removal_alert_proceed":"Продолжить", + "authorities_removal_alert_title":"Удалить организацию", + "authorities_title":"Надёжные источники", + "choose_provider_subtitle":"Чтобы получать информацию о возможных рисках, вам нужно подписаться на орган здравоохранения.", + "choose_provider_title":"Выбрать орган здравоохранения", + "commitment":"Гарантии", + "commitment_para":"Safe Paths ведет безопасную запись и отслеживание вашего взаимодействия с другими людьми, используя вашу геопозицию. При этом данные о вашем местонахождении НИКОГДА не передаются с вашего устройства без вашего согласия.", + "default_news_site_name":"Новости Safe Paths", + "event_history_subtitle":"Оцените собственные риски на основе информации от органов здравоохранения.", + "event_history_title":"История возможных рисков", + "export_para_1":"Если ваш тест на COVID-19 оказался положительным, пожалуйста, поделитесь историей своих перемещений с местными ответственными организациями.", + "export_para_2":"Местоположение передается в виде простого списка координат и времени, без дополнительной информации.", + "home_at_risk_header":"Возможно, вы в опасной зоне", + "home_at_risk_subsubtext":"Это не значит, что вы заразились.", + "home_at_risk_subtext":"Судя по вашей GPS истории, есть вероятность, что вы контактировали или находились рядом с человеком, инфицированным COVID-19.", + "home_enable_location":"Включить данные геопозиции", + "home_mayo_link_heading":"Подробнее о COVID-19", + "home_mayo_link_label":"от Kлиники Мэйо", + "home_next_steps":"Подробнее", + "home_no_contact_header":"Контактов не обнаружено", + "home_no_contact_subtext":"Согласно имеющимся данным, вы не находились рядом с человеком, больным COVID-19.", + "home_unknown_header":"Неизвестно", + "home_unknown_subtext":"Мы не сможем сказать, подвергались ли вы риску, пока вы не разрешите приложению доступ к вашей геопозиции", + "import_step_1":"1. Войти с помощью Google и скачать историю ваших перемещений", + "import_step_2":"2. После окончания загрузки откройте это окно заново. Данные импортируются автоматически.", + "import_title":"Импортировать местонахождения", + "latest_news":"Последние новости", + "launch_done_header":"Готово", + "launch_done_subheader":"Можно начинать. Помните, что вы в любой момент можете изменить настройки.", + "launch_enable_location":"Включить отслеживание местоположения", + "launch_enable_notif":"Включить уведомления", + "launch_finish_set_up":"Завершить настройку", + "launch_get_started":"Начать", + "launch_location_access":"Доступ к местоположению", + "launch_location_header":"Чтобы помнить, где вы были, телефону необходимо сохранять ваше местоположение.", + "launch_location_subheader":"Будьте уверены, данные хранятся только на вашем телефоне, если вы явно не решите поделиться.", + "launch_next":"Далее", + "launch_notif_header":"Вы получите уведомление, если вы окажетесь вблизи инфицированного человека.", + "launch_notif_subheader":"Мы побеспокоим вас только в случае риска попадания в опасную зону.", + "launch_notification_access":"Разрешить уведомления", + "launch_screen1_header":"Возвращение к привычной жизни начинается здесь.", + "launch_screen2_header":"Получайте уведомления, если вы были рядом с человеком, у которого впоследствии был диагностирован COVID-19.", + "launch_screen2_subheader":"Знание – сила.", + "launch_screen3_header":"Если ваш тест на COVID-19 оказался положительным, вы можете анонимно предоставить свои данные", + "launch_screen3_subheader":"Это способствует безопасности всего вашего окружения.", + "launch_screen4_header":"Всё под вашим контролем. Данные сохраняются только на вашем телефоне.", + "launch_screen4_subheader":"Если ваш тест на COVID-19 оказался положительным, только вы решаете, делиться ли данной информацией.", + "launch_set_up_phone":"Настроить телефон", + "legal_page_title":"Правовая информация", + "less_than_one_minute":"менее 1 минуты", + "loading_public_data":"идёт загрузка данных…", + "location_disabled_message":"Для работы COVID Safe Paths требуются службы геолокации.", + "location_disabled_title":"Отслеживание местонахождения было отключено", + "location_enabled_message":"COVID Safe Paths ведёт безопасную запись ваших GPS координат на данном устройстве раз в пять минут.", + "location_enabled_title":"COVID Safe Paths включено", + "maps_import_button_text":"Импортировать последние местонахождения", + "maps_import_disclaimer":"Safe Paths не является партнером Google и не распространяет ваши данные.", + "maps_import_text":"Чтобы узнать, не контактировали ли вы с больным COVID-19 до установки приложения, импортируйте историю своих перемещений. ", + "maps_import_title":"Google Карты", + "nCoV2019_url_info":"Подробнее о наборе данных этой карты", + "news_subtitle":"Будьте в курсе последних данных о COVID от вашей организации здравоохранения и не только.", + "news_title":"Последние новости", + "no_data":"Нет данных", + "notification_2_weeks_ago":"2 недели назад", + "notification_data_not_available":"Нет данных о возможных рисках заражения", + "notification_random_data_button":"Выбрать орган здравоохранения", + "notification_title":"Профиль рисков за 2 недели", + "notification_today":"сегодня", + "notification_warning_text":"Если в вашем регионе есть органы здравоохранения, вы можете подписаться на них, чтобы получать новости о рисках заражения.", + "notifications_exposure_format":"{{daysAgo}} дней назад вы находились рядом с инфицированным человеком в течение {{exposureTime}} минут.", + "notifications_exposure_format_today":"Сегодня вы находились рядом с инфицированным человеком в течение {{exposureTime}} минут.", + "notifications_exposure_format_yesterday":"Вчера вы находились рядом с инфицированным человеком в течение {{exposureTime}} минут.", + "notifications_no_exposure":"Нет выявленных мест возможного заражения COVID-19 за последние две недели.", + "overlap_found_button_label":"Публичные данные загружены", + "overlap_no_results_button_label":"Публичные данные загружены", + "overlap_para_1":"Зелёным отмечена история ваших перемещений\\n\\nБледно-фиолетовые круги обозначают публичный набор данных", + "overlap_title":"Смотреть зоны пересечения", + "push_at_risk_message":"Ваш путь пересекался с путем человека, больным COVID-19", + "push_at_risk_title":"Возможно, вы в зоне риска", + "settings_title":"Контрольная панель", + "share_location_data":"Поделится данными о местоположении", + "show_overlap":"Нажмите, чтобы увидеть публичный набор данных", + "team":"Команда", + "team_para":"Наша команда состоит из эпидемиологов, инженеров, специалистов по обработке данных, евангелистов цифровой безопасности, профессоров и исследователей институтов с мировым именем, таких как МТИ (MIT), Гарвард, Клиника Мэйо, TripleBlind, EyeNetra, Ernst & Young и Link Ventures.", + "terms_of_use":"Условия использования", + "tested_positive_subtitle":"Ваши личные данные могут быть предоставлены органам здравоохранения, сохранены в резервную копию или переданы иным образом.", + "tested_positive_title":"Поделиться историей перемещений" + } +} diff --git a/i18next-parser.config.js b/i18next-parser.config.js index 84d0a756e4..ea47de177f 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -44,6 +44,7 @@ module.exports = { 'es', 'ht', 'it', + 'ru', 'zh-Hant', ], // An array of the locales in your applications From 0d6f7da6e1c66d49072f2507a0330222c13c9282 Mon Sep 17 00:00:00 2001 From: Mihail Lemeza <42576152+darkerthanblackoff@users.noreply.github.com> Date: Tue, 14 Apr 2020 19:07:12 +0300 Subject: [PATCH 08/12] Notify "Location tracking was disabled" on Android (#503) --- app/services/LocationService.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/services/LocationService.js b/app/services/LocationService.js index 8e93c9bd71..0bf8e787fb 100644 --- a/app/services/LocationService.js +++ b/app/services/LocationService.js @@ -9,6 +9,7 @@ import languages from '../locales/languages'; import { isPlatformAndroid } from '../Util'; let isBackgroundGeolocationConfigured = false; +const LOCATION_DISABLED_NOTIFICATION = '55'; export class LocationData { constructor() { @@ -246,8 +247,19 @@ export default class LocationServices { } else { BackgroundGeolocation.start(); //triggers start on start event - // TODO: We reach this point on Android when location services are toggled off/on. - // When this fires, check if they are off and show a Notification in the tray + BackgroundGeolocation.checkStatus(({ locationServicesEnabled }) => { + if (!locationServicesEnabled) { + PushNotification.localNotification({ + id: LOCATION_DISABLED_NOTIFICATION, + title: languages.t('label.location_disabled_title'), + message: languages.t('label.location_disabled_message'), + }); + } else { + PushNotification.cancelLocalNotifications({ + id: LOCATION_DISABLED_NOTIFICATION, + }); + } + }); } }); @@ -273,8 +285,8 @@ export default class LocationServices { BackgroundGeolocation.on('stop', () => { PushNotification.localNotification({ - title: 'Location Tracking Was Disabled', - message: 'COVID Safe Paths requires location services.', + title: languages.t('label.location_disabled_title'), + message: languages.t('label.location_disabled_message'), }); console.log('[INFO] stop'); }); From 943e961e4681776ae113d48bb7326c8ef6e0db35 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Tue, 14 Apr 2020 18:14:51 +0200 Subject: [PATCH 09/12] Add French translation (#490) --- app/locales/fr.json | 99 ++++++++++++++++++++++++++++++++++++++++ app/locales/languages.js | 2 + i18next-parser.config.js | 1 + 3 files changed, 102 insertions(+) create mode 100644 app/locales/fr.json diff --git a/app/locales/fr.json b/app/locales/fr.json new file mode 100644 index 0000000000..dd4af93c74 --- /dev/null +++ b/app/locales/fr.json @@ -0,0 +1,99 @@ +{ + "label": { + "about_title": "À propos", + "authorities_add_button_label": "Ajouter une source de confiance", + "authorities_add_url": "Ajouter une autorité par URL", + "authorities_desc": "Choisissez une autorité sanitaire de confiance dans votre région pour obtenir les données d’exposition. Vous pouvez sélectionner un nom dans la liste globale, ou entrer l’adresse web fournie par une autorité qui a mis en place Safe Paths.", + "authorities_input_placeholder": "Collez votre URL ici", + "authorities_no_sources": "Aucune source ajoutée", + "authorities_removal_alert_cancel": "Annuler", + "authorities_removal_alert_desc": "Êtes-vous sûr·e de vouloir retirer cette autorité de sources de données?", + "authorities_removal_alert_proceed": "Continuer", + "authorities_removal_alert_title": "Supprimer une autorité", + "authorities_title": "Sources de confiance", + "choose_provider_subtitle": "Pour vous tenir informé·e des risques d’exposition, vous devez souscrire à une autorité sanitaire.", + "choose_provider_title": "Choisissez votre autorité sanitaire", + "commitment": "Engagement", + "commitment_para": "Safe Paths enregistre votre position de façon sécurisée et l’utilise pour déterminer votre risque d’exposition. Vos données ne quitteront JAMAIS votre appareil sans votre consentement.", + "default_news_site_name": "Nouvelles SafePaths (en anglais)", + "event_history_subtitle": "Comprendre son exposition personnelle à partie des informations partagées par les autorités sanitaires.", + "event_history_title": "Historique d’exposition", + "export_para_1": "Si vous êtes diagnostiqué·e positif·ve à la COVID-19, partager votre historique de positions avec les autorités locales contribuera à endiguer la maladie.", + "export_para_2": "L’historique partagé est une simple liste de lieux et de dates, sans informations supplémentaires.", + "home_at_risk_header": "Vous avez pu être exposé·e", + "home_at_risk_subsubtext": "Ceci ne signifie pas nécessairement que vous êtes contaminés.", + "home_at_risk_subtext": "D’après votre historique de positions GPS, il est possible que vous ayez été en contact ou à proximité d’une personne porteuse du COVID-19.", + "home_enable_location": "Activer les données de position", + "home_mayo_link_heading": "Plus d’informations sur la COVID-19", + "home_mayo_link_label": "de l’ONG Mayo Clinic (en anglais)", + "home_next_steps": "En savoir plus", + "home_no_contact_header": "Pas de contacts connus", + "home_no_contact_subtext": "D’après les données disponibles, vous n’avez pas été à proximité de porteur·ses du COVID-19.", + "home_unknown_header": "Pas de données", + "home_unknown_subtext": "Il est nécessaire d’autoriser l’application à accéder à votre position pour déterminer les risques d’exposition.", + "import_step_1": "1. Connectez-vous à votre compte Google et téléchargez votre Historique des positions", + "import_step_2": "2. Une fois le téléchargement fini, ouvrez de nouveau cette page. Les données seront importées automatiquement.", + "import_title": "Importer les positions", + "latest_news": "Dernières nouvelles", + "launch_done_header": "Terminé", + "launch_done_subheader": "Tout est prêt. Rappelez vous que vous pouvez toujours revenir ajuster vos préférences.", + "launch_enable_location": "Activer la position", + "launch_enable_notif": "Activer les notifications", + "launch_finish_set_up": "Terminer la configuration", + "launch_get_started": "Commencer", + "launch_location_access": "Accès à la position", + "launch_location_header": "Pour se rappeler où vous êtes allé·e, votre appareil doit enregistrer vos positions", + "launch_location_subheader": "Ne vous inquiétez pas, aucune information ne quitte votre appareil sans que vous la partagiez explicitement.", + "launch_next": "Suivant", + "launch_notif_header": "Les notifications vous permettront de savoir si vous croisez une personne infectée.", + "launch_notif_subheader": "Nous ne vous dérangerons que pour vous tenir à jour de vos risques d’exposition.", + "launch_notification_access": "Autoriser les notifications", + "launch_screen1_header": "Le retour à la normal commence ici.", + "launch_screen2_header": "Recevez une notification si vous avez croisé une personne qui a été ultérieurement diagnostiquée positive à la COVID-19.", + "launch_screen2_subheader": "Le savoir, c'est le pouvoir.", + "launch_screen3_header": "Si vous êtes diagnostiqué·e positif·ve à la COVID-19, vous pourrez partager vos données anonymement", + "launch_screen3_subheader": "ce qui contribuera à endiguer la maladie autour de vous.", + "launch_screen4_header": "Vous êtes aux commandes. Vos données ne sont sauvegardées que sur votre appareil.", + "launch_screen4_subheader": "Si vous êtes diagnostiqué·e positif·ve, vous seul·e pourrez choisir de partager vos données.", + "launch_set_up_phone": "Configurer mon appareil", + "legal_page_title": "Juridique", + "less_than_one_minute": "moins d’une minute", + "loading_public_data": "chargement des données...", + "location_disabled_message": "COVID Safe Paths a besoin des services de position.", + "location_disabled_title": "Le suivi de la position a été désactivé", + "location_enabled_message": "COVID Safe Paths enregistre de façon sécurisée vos coordonnées GPS toutes les cinq minutes sur cet appareil..", + "location_enabled_title": "COVID Safe Paths activé", + "maps_import_button_text": "Importer les positions passées", + "maps_import_disclaimer": "L’application Safe Paths n’est pas affiliée à Google et ne partage jamais vos données.", + "maps_import_text": "Vous pouvez importer votre historique personnel de position pour voir si vous avez croisé une personne infectée avant d’avoir téléchargé cette application.", + "maps_import_title": "Google Maps", + "nCoV2019_url_info": "Plus d’informations sur les données représentées sur cette carte", + "news_subtitle": "Obtenez les dernières nouvelles sur le COVID-19 par vos autorités sanitaires et en général.", + "news_title": "Dernières nouvelles", + "no_data": "Pas de données", + "notification_2_weeks_ago": "Il y a 2 semaines", + "notification_data_not_available": "Pas de données d’exposition disponibles.", + "notification_random_data_button": "Choisir une autorité sanitaire", + "notification_title": "Profil des expositions sur 2 semaines", + "notification_today": "aujourd’hui", + "notification_warning_text": "Si une autorité sanitaire dans votre région a mis en place Safe Paths, vous pouvez abonner pour obtenir des mises à jours régulières sur les risques d’exposition.", + "notifications_exposure_format": "Vous avez croisé une personne infectée il y a {{daysAgo}} jours pendant {{exposureTime}} minutes.", + "notifications_exposure_format_today": "Vous avez croisé une personne infectée aujourd’hui pendant {{exposureTime}} minutes.", + "notifications_exposure_format_yesterday": "Vous avez croisé une personne infectée hier pendant {{exposureTime}} minutes.", + "notifications_no_exposure": "Pas d’expositions connues à la COVID-19 durant les deux dernières semaines.", + "overlap_found_button_label": "Données publiques chargées", + "overlap_no_results_button_label": "Données publiques chargées", + "overlap_para_1": "Le chemin vert représente votre historique de positions\n\nLes cercles violets clairs représentes les données publiques", + "overlap_title": "Chercher les recoupements", + "push_at_risk_message": "Vous avez croisé une personne infectée à la COVID-19", + "push_at_risk_title": "Vous avez pu être exposé", + "settings_title": "Réglages", + "share_location_data": "Partager les données de position", + "show_overlap": "Cliquez pour voir les données publiques", + "team": "L’équipe", + "team_para": "Notre équipe est un consortium composé d’épidémiologistes, d’ingénieur·es, de scientifiques des données, de défenseurs de la vie privée numérique, de professeur·es et de chercheur·ses d’institutions réputées, dont le MIT, Harvard, The Mayo Clinic, TripleBlind, EyeNetra, Ernst & Young, et Link Ventures.", + "terms_of_use": "Conditions d’utilisation (en anglais)", + "tested_positive_subtitle": "Vos données de position peuvent être envoyées aux autorités sanitaires, sauvegardées, ou partagées autrement.", + "tested_positive_title": "Partager votre historique de position" + } +} diff --git a/app/locales/languages.js b/app/locales/languages.js index 36574e3bdd..a012a6e623 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -5,6 +5,7 @@ import { LANG_OVERRIDE } from '../constants/storage'; import { GetStoreData } from '../helpers/General'; import en from './en.json'; import es from './es.json'; +import fr from './fr.json'; import ht from './ht.json'; import it from './it.json'; import ru from './ru.json'; @@ -60,6 +61,7 @@ i18next.init({ resources: { en: { label: 'English', translation: en }, es: { label: 'Español', translation: es }, + fr: { label: 'Français', translation: fr }, ht: { label: 'Kreyòl ayisyen', translation: ht }, it: { label: 'Italiano', translation: it }, ru: { label: 'Русский', translation: ru }, diff --git a/i18next-parser.config.js b/i18next-parser.config.js index ea47de177f..fdc1456245 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -42,6 +42,7 @@ module.exports = { // sort alphabetically 'en', 'es', + 'fr', 'ht', 'it', 'ru', From 66c31e1fb1f1f6414be9cf185b0ee097b1bafe42 Mon Sep 17 00:00:00 2001 From: Tim Stirrat Date: Tue, 14 Apr 2020 09:28:01 -0700 Subject: [PATCH 10/12] Move Google import block, disable for release builds (#505) Disabled with "Coming soon" until #410 is merged --- app/views/Settings.js | 34 +- .../__snapshots__/Settings.spec.js.snap | 439 +++++++++--------- 2 files changed, 235 insertions(+), 238 deletions(-) diff --git a/app/views/Settings.js b/app/views/Settings.js index 5c984ff050..9d58ea6107 100644 --- a/app/views/Settings.js +++ b/app/views/Settings.js @@ -47,7 +47,9 @@ class SettingsScreen extends Component { } importButtonPressed() { - this.props.navigation.navigate('ImportScreen'); + if (__DEV__) { + this.props.navigation.navigate('ImportScreen'); + } } aboutButtonPressed() { @@ -90,7 +92,11 @@ class SettingsScreen extends Component { */} - - {this.getMapsImport()} - - - - - {this.getSettingRow( @@ -170,7 +169,7 @@ class SettingsScreen extends Component { null, languages.t('label.choose_provider_subtitle'), )} - + {this.getSettingRow( languages.t('label.news_title'), this.newsButtonPressed, @@ -178,7 +177,7 @@ class SettingsScreen extends Component { null, languages.t('label.news_subtitle'), )} - + {this.getSettingRow( languages.t('label.event_history_title'), this.eventHistoryButtonPressed, @@ -186,7 +185,8 @@ class SettingsScreen extends Component { null, languages.t('label.event_history_subtitle'), )} - + + {this.getSettingRow( languages.t('label.tested_positive_title'), this.testedPositiveButtonPressed, @@ -196,18 +196,24 @@ class SettingsScreen extends Component { )} - + + + + {this.getMapsImport()} + + + {this.getSettingRow( languages.t('label.about_title'), this.aboutButtonPressed, )} - + {this.getSettingRow( languages.t('label.legal_page_title'), this.licensesButtonPressed, diff --git a/app/views/__tests__/__snapshots__/Settings.spec.js.snap b/app/views/__tests__/__snapshots__/Settings.spec.js.snap index 6d1b5e178e..fa176bbeae 100644 --- a/app/views/__tests__/__snapshots__/Settings.spec.js.snap +++ b/app/views/__tests__/__snapshots__/Settings.spec.js.snap @@ -91,15 +91,6 @@ exports[`renders correctly 1`] = ` - - - - - - - - - - -" - /> - - Google Maps - - - - To see if you encountered someone with COVID-19 prior to downloading this app, you can import your personal location history. - - - - - - - Import past locations - - - - - + Choose health authority + - Safe Paths has no affiliation with Google and never shares your data. + To be informed of exposures you will need to subscribe to a health authority. - - - - - - - + /> - Choose health authority + Latest news - To be informed of exposures you will need to subscribe to a health authority. + Read about the latest COVID updates from your health authority and in general. - Latest news + Exposure history - Read about the latest COVID updates from your health authority and in general. + Understand your personal exposure based on information shared by health authorities. - Exposure history + Share location history - Understand your personal exposure based on information shared by health authorities. + Your private data can be transferred to health authorities, backed up, or otherwise shared. + + + + + + + + > + + + + + + + + + +" + /> + + Google Maps + + + To see if you encountered someone with COVID-19 prior to downloading this app, you can import your personal location history. + + + + - Share location history - + + + Import past locations + + + + + - Your private data can be transferred to health authorities, backed up, or otherwise shared. + Safe Paths has no affiliation with Google and never shares your data. From 3e6c0795dabba0b899d3571bcef19a1816b6109c Mon Sep 17 00:00:00 2001 From: Tim Stirrat Date: Tue, 14 Apr 2020 09:31:10 -0700 Subject: [PATCH 11/12] Event history UX update (#467) * Install emotion * Refactor screens * Set moment locale to device or override locale * Add moment-range * Extract some i18n text, change scrolling area * Add month grid * Show today on calendar view * Remove some cruft * Add some tests * Change the CTA on main screen * update snaps * Use emotion themeing for better themeyness * ' => ' * revert prettier change * Add monospace option * Updated Docs * Use enum * Use date instead of daysAgo for better testing * Export fn * moment -> dayjs * headline3 style * Revert color change * Add MonthGrid tests * More tests * Added docs for Typography and Theme * Typography tests * Theme tests * mono -> monochrome theme * dar -> charcoal, main -> default, inverted -> violet * Fix yarn.lock * Remove timeline heading * Add legend below calendar * Mock date more properly * fix broken test * DataCircle tests * Handle no history * Be more theme aware * NavBar is now theme aware * calendar view padding change * Update snapshots * Show today's risk if known * do not switch title on expose/no known * Show possible risk more important than "today" * Use theme background in nav wrapper, if present * Add month title * Update snaps for day circles * revert bg color which breaks animations * Clarify i18n * Show green above today, if mins is 0 * Add snap tests for orange/green being higher prio than today --- .prettierignore | 1 - app/Entry.js | 6 +- app/Util.js | 4 +- app/components/NavigationBarWrapper.js | 154 +-- app/components/Typography.js | 92 ++ app/components/__tests__/Typography.spec.js | 40 + .../__snapshots__/Typography.spec.js.snap | 96 ++ app/constants/__tests__/Theme.spec.js | 30 + .../__snapshots__/Theme.spec.js.snap | 77 ++ app/constants/colors.js | 14 +- app/constants/history.js | 24 + app/constants/themes.js | 122 +++ app/locales/all-dayjs-locales.js | 133 +++ app/locales/en.json | 14 +- app/locales/languages.js | 14 + app/views/ExposureHistory/CalendarDay.js | 65 ++ app/views/ExposureHistory/DataCircle.js | 87 ++ app/views/ExposureHistory/DetailedHistory.js | 67 ++ .../ExposureHistory/ExposureCalendarView.js | 99 ++ app/views/ExposureHistory/ExposureHistory.js | 133 +++ app/views/ExposureHistory/MonthGrid.js | 96 ++ .../ExposureHistory/SingleExposureDetail.js | 96 ++ .../__tests__/CalendarDay.spec.js | 77 ++ .../__tests__/DataCircle.spec.js | 55 ++ .../__tests__/ExposureHistory.spec.js | 48 + .../__tests__/MonthGrid.spec.js | 52 + .../__tests__/SingleExposureDetails.spec.js | 15 + .../__snapshots__/CalendarDay.spec.js.snap | 379 ++++++++ .../__snapshots__/DataCircle.spec.js.snap | 246 +++++ .../__snapshots__/MonthGrid.spec.js.snap | 907 ++++++++++++++++++ .../SingleExposureDetails.spec.js.snap | 179 ++++ app/views/LocationTracking.js | 4 +- app/views/Notification.js | 359 ------- app/views/Settings.js | 2 +- app/views/__tests__/Overlap.spec.js | 4 +- .../__snapshots__/Import.spec.js.snap | 8 +- .../__snapshots__/Licenses.spec.js.snap | 8 +- .../__tests__/__snapshots__/News.spec.js.snap | 8 +- .../__snapshots__/Settings.spec.js.snap | 8 +- jestSetupFile.js | 8 + package.json | 6 +- yarn.lock | 189 +++- 42 files changed, 3562 insertions(+), 464 deletions(-) create mode 100644 app/components/Typography.js create mode 100644 app/components/__tests__/Typography.spec.js create mode 100644 app/components/__tests__/__snapshots__/Typography.spec.js.snap create mode 100644 app/constants/__tests__/Theme.spec.js create mode 100644 app/constants/__tests__/__snapshots__/Theme.spec.js.snap create mode 100644 app/constants/history.js create mode 100644 app/constants/themes.js create mode 100644 app/locales/all-dayjs-locales.js create mode 100644 app/views/ExposureHistory/CalendarDay.js create mode 100644 app/views/ExposureHistory/DataCircle.js create mode 100644 app/views/ExposureHistory/DetailedHistory.js create mode 100644 app/views/ExposureHistory/ExposureCalendarView.js create mode 100644 app/views/ExposureHistory/ExposureHistory.js create mode 100644 app/views/ExposureHistory/MonthGrid.js create mode 100644 app/views/ExposureHistory/SingleExposureDetail.js create mode 100644 app/views/ExposureHistory/__tests__/CalendarDay.spec.js create mode 100644 app/views/ExposureHistory/__tests__/DataCircle.spec.js create mode 100644 app/views/ExposureHistory/__tests__/ExposureHistory.spec.js create mode 100644 app/views/ExposureHistory/__tests__/MonthGrid.spec.js create mode 100644 app/views/ExposureHistory/__tests__/SingleExposureDetails.spec.js create mode 100644 app/views/ExposureHistory/__tests__/__snapshots__/CalendarDay.spec.js.snap create mode 100644 app/views/ExposureHistory/__tests__/__snapshots__/DataCircle.spec.js.snap create mode 100644 app/views/ExposureHistory/__tests__/__snapshots__/MonthGrid.spec.js.snap create mode 100644 app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap delete mode 100644 app/views/Notification.js diff --git a/.prettierignore b/.prettierignore index 5d008c0c27..a60faef7ad 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,7 +2,6 @@ node_modules/ ios/ android/ patches/ -__tests__/ package-lock.json yarn.lock package.json diff --git a/app/Entry.js b/app/Entry.js index 801b385c24..4ac7a50caf 100644 --- a/app/Entry.js +++ b/app/Entry.js @@ -10,12 +10,12 @@ import { GetStoreData } from './helpers/General'; import AboutScreen from './views/About'; import ChooseProviderScreen from './views/ChooseProvider'; import ExportScreen from './views/Export'; +import { ExposureHistoryScreen } from './views/ExposureHistory/ExposureHistory'; import ImportScreen from './views/Import'; import LicencesScreen from './views/Licenses'; import LocationTracking from './views/LocationTracking'; import MapLocation from './views/MapLocation'; import NewsScreen from './views/News'; -import NotificationScreen from './views/Notification'; import Onboarding1 from './views/onboarding/Onboarding1'; import Onboarding2 from './views/onboarding/Onboarding2'; import Onboarding3 from './views/onboarding/Onboarding3'; @@ -135,8 +135,8 @@ class Entry extends Component { options={{ headerShown: false }} /> - - - - - this.props.onBackPress()}> - - - {this.props.title} - - {this.props.children} - - - ); - } -} +/** + * Navigation bar and status bar + * + * @param {{ + * title: string, + * onBackPress: () => void, + * }} param0 + */ +const NavigationBarWrapper = ({ children, title, onBackPress }) => { + const theme = useTheme(); -const styles = StyleSheet.create({ - topSafeAreaContainer: { - flex: 0, - backgroundColor: Colors.VIOLET, - }, - bottomSafeAreaContainer: { - flex: 1, - backgroundColor: Colors.INTRO_WHITE_BG, - }, - headerContainer: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: Colors.NAV_BAR_VIOLET, - backgroundColor: Colors.VIOLET, - }, - headerTitle: { - fontSize: 26, - fontFamily: fontFamily.primaryMedium, - color: Colors.WHITE, - position: 'absolute', - alignSelf: 'center', - textAlign: 'center', - width: '100%', - }, - backArrowTouchable: { - width: 60, - height: 55, - justifyContent: 'center', - alignItems: 'center', - zIndex: 1, - }, - backArrow: { - height: 18, - width: 18, - }, -}); + const barColor = (theme && theme.navBar) || Colors.VIOLET; + + return ( + <> + + + +
+ onBackPress()}> + + + {title} +
+ {children} +
+ + ); +}; + +const themeNavBar = ({ theme }) => theme.navBar || Colors.VIOLET; + +// TODO: this breaks transitions... +// const themeBackground = ({ theme }) => +// theme.background || Colors.INTRO_WHITE_BG; + +const TopContainer = styled.SafeAreaView` + flex: 0; + background-color: ${themeNavBar}; +`; + +const BottomContainer = styled.SafeAreaView` + flex: 1; + background-color: ${Colors.INTRO_WHITE_BG}; +`; + +const themeNavBarBorder = ({ theme }) => + theme.navBarBorder || Colors.NAV_BAR_VIOLET; + +const Header = styled.View` + background-color: ${themeNavBar}; + border-bottom-color: ${themeNavBarBorder}; + border-bottom-width: 1px; + flex-direction: row; +`; + +const Title = styled.Text` + align-self: center; + color: ${Colors.WHITE}; + font-family: IBMPlexSans-Medium; + font-size: 26px; + position: absolute; + text-align: center; + width: 100%; +`; + +const BackArrow = styled.TouchableOpacity` + align-items: center; + height: 55px; + justify-content: center; + width: 60px; + z-index: 1; +`; + +const BackArrowIcon = styled(SvgXml)` + height: 18px; + width: 18px; +`; NavigationBarWrapper.propTypes = { title: PropTypes.string.isRequired, diff --git a/app/components/Typography.js b/app/components/Typography.js new file mode 100644 index 0000000000..f2478d1b40 --- /dev/null +++ b/app/components/Typography.js @@ -0,0 +1,92 @@ +import * as React from 'react'; +import styled from '@emotion/native'; + +export const Type = { + Headline1: 'headline1', + Headline2: 'headline2', + Headline3: 'headline3', + Body1: 'body1', + Body2: 'body2', + Body3: 'body3', +}; + +/** + * Render a theme and visual style aware text element. + * + * It uses the theme's `text(Primary|Secondary)OnBackground` color and a set of + * predefined font size and line height values. + * + * Inspired by: https://material-components.github.io/material-components-web-catalog/#/component/typography + * + * Usage: + * + * ```jsx + * + * Heading + * Paragraph text ... + * link + * + * ``` + * + * Use within a `` + * + * @param {{ + * use?: string, + * secondary?: boolean, + * monospace?: boolean, + * }} param0 + */ +export const Typography = ({ + use = Type.Body1, + secondary, + monospace, + children, + ...otherProps +}) => { + return ( + + {children} + + ); +}; + +const FONT_SIZE_MAP = { + [Type.Headline1]: '52px', + [Type.Headline2]: '26px', + [Type.Headline3]: '16px', + [Type.Body1]: '18px', + [Type.Body2]: '16px', + [Type.Body3]: '15px', +}; + +const getFontSize = ({ use = Type.Body1 }) => FONT_SIZE_MAP[use]; + +const LINE_HEIGHT_MAP = { + [Type.Headline1]: '48px', + [Type.Headline2]: '34px', + [Type.Headline3]: '40px', + [Type.Body1]: '24px', + [Type.Body2]: '22px', + [Type.Body3]: '24px', +}; + +const getLineHeight = ({ use = Type.Body1 }) => LINE_HEIGHT_MAP[use]; + +const getTextColor = ({ theme, secondary = false }) => + secondary ? theme.textSecondaryOnBackground : theme.textPrimaryOnBackground; + +const getFontWeight = ({ use = Type.Body1 }) => + use.startsWith('headline') ? 'bold' : 'normal'; + +const ThemedText = styled.Text` + color: ${getTextColor}; + font-family: ${({ monospace }) => + monospace ? 'IBM Plex Mono' : 'IBM Plex Sans'}; + font-size: ${getFontSize}; + font-weight: ${getFontWeight}; + line-height: ${getLineHeight}; +`; diff --git a/app/components/__tests__/Typography.spec.js b/app/components/__tests__/Typography.spec.js new file mode 100644 index 0000000000..512c3bb812 --- /dev/null +++ b/app/components/__tests__/Typography.spec.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; + +import { Typography } from '../Typography'; +import { Theme } from '../../constants/themes'; + +it('headline1 is large and bold', () => { + const { asJSON } = render( + + headline1 + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('body1 is regular', () => { + const { asJSON } = render( + + body1 + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('changes color based on theme', () => { + const { asJSON } = render( + <> + + white + + + violet + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); diff --git a/app/components/__tests__/__snapshots__/Typography.spec.js.snap b/app/components/__tests__/__snapshots__/Typography.spec.js.snap new file mode 100644 index 0000000000..1b28c76f0f --- /dev/null +++ b/app/components/__tests__/__snapshots__/Typography.spec.js.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`body1 is regular 1`] = ` + + + body1 + + +`; + +exports[`changes color based on theme 1`] = ` + + + white + + + violet + + +`; + +exports[`headline1 is large and bold 1`] = ` + + + headline1 + + +`; diff --git a/app/constants/__tests__/Theme.spec.js b/app/constants/__tests__/Theme.spec.js new file mode 100644 index 0000000000..a19db42f22 --- /dev/null +++ b/app/constants/__tests__/Theme.spec.js @@ -0,0 +1,30 @@ +import { render } from '@testing-library/react-native'; +import React from 'react'; + +import { Typography } from '../../components/Typography'; +import { Theme } from '../themes'; + +it('includes extra background View if setBackground=true', () => { + const { asJSON } = render( + + Text + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('changes text color based on theme', () => { + const { asJSON } = render( + <> + + Text + + + Text + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); diff --git a/app/constants/__tests__/__snapshots__/Theme.spec.js.snap b/app/constants/__tests__/__snapshots__/Theme.spec.js.snap new file mode 100644 index 0000000000..134c260d34 --- /dev/null +++ b/app/constants/__tests__/__snapshots__/Theme.spec.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`changes text color based on theme 1`] = ` + + + Text + + + Text + + +`; + +exports[`includes extra background View if setBackground=true 1`] = ` + + + + Text + + + +`; diff --git a/app/constants/colors.js b/app/constants/colors.js index c105db7a8b..b26014fd8f 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -5,9 +5,10 @@ const colors = { SILVER: '#BEBEBE', TORCH_RED: '#F8262F', MISCHKA: '#E5E4E6', - APP_BACKGROUND: '#FFF8ED', PRIMARY_TEXT: '#000', - GREEN: '#32A852', + SUCCESS: '#41dca4', // Green + WARNING: '#ffc000', // Orange + VIOLET_ALPHA_06: 'rgba(105, 121, 248, 0.06)', VIOLET: '#4051DB', TRANSPARENT: 'transparent', @@ -24,7 +25,7 @@ const colors = { VIOLET_BUTTON: '#6979F8', VIOLET_BUTTON_DARK: '#3A4CD7', - VIOLET_TEXT: '#6979F8', + VIOLET_TEXT: '#3C4ED8', GRAY_BACKGROUND: '#DADADA', @@ -46,11 +47,16 @@ const colors = { BLUE_LINK: '#007AFF', - VIOLET_TEXT: '#3C4ED8', RED_TEXT: '#FF5656', NAV_BAR_VIOLET: '#5061E6', NAV_BAR_BORDER: 'rgba(189, 195, 199,0.6)', + + DARK_GRAY: '#3C475B', + LIGHT_GRAY: '#EEEEEE', + + MONO_DARK: '#202020', + MONO_SECONDARY: '#757677', }; export default colors; diff --git a/app/constants/history.js b/app/constants/history.js new file mode 100644 index 0000000000..5c9b6286d1 --- /dev/null +++ b/app/constants/history.js @@ -0,0 +1,24 @@ +/** + * Max esposure reporting window in days + */ +export const MAX_EXPOSURE_WINDOW = 14; + +/** + * The value in minutes of each "bin" in the crossed path data. + */ +export const BIN_DURATION = 5; + +/** + * Format of a single history item + * + * @typedef {{ + * date: import("dayjs").Dayjs, + * exposureMinutes: number, + * }} HistoryDay + */ + +/** + * Exposure history + * + * @typedef {!HistoryDay[]} History + */ diff --git a/app/constants/themes.js b/app/constants/themes.js new file mode 100644 index 0000000000..1ffb00e2c7 --- /dev/null +++ b/app/constants/themes.js @@ -0,0 +1,122 @@ +import styled from '@emotion/native'; +import { ThemeProvider } from 'emotion-theming'; +import React from 'react'; + +import Color from './colors'; + +/** Violet on pale violet bg. e.g. Settings */ +export const defaultTheme = { + background: Color.VIOLET_ALPHA_06, + textPrimaryOnBackground: Color.VIOLET, + textSecondaryOnBackground: 'rgba(64, 81, 219, 0.6)', + + navBar: Color.VIOLET, + onNavBar: Color.WHITE, + + /** E.g. button bg */ + primary: Color.VIOLET_BUTTON, + /** E.g. button text color */ + onPrimary: Color.WHITE, + + success: Color.SUCCESS, + warning: Color.WARNING, + disabled: 'rgba(64, 81, 219, 0.6)', + border: Color.VIOLET_BUTTON, +}; + +/** White on violet bg. E.g. Main screen */ +export const violet = { + ...defaultTheme, + background: `linear-gradient(245.21deg, ${Color.VIOLET_BUTTON} 0%, ${Color.VIOLET_BUTTON_DARK} 100%);`, + textPrimaryOnBackground: Color.WHITE, + textSecondaryOnBackground: 'rgba(255, 255, 255, 0.6)', + + navBarBorder: Color.VIOLET, // no visible border + + primary: Color.WHITE, + onPrimary: Color.VIOLET, +}; + +/** White on gray bg. E.g. Possible exposure mode on default screen */ +export const charcoal = { + ...defaultTheme, + background: Color.DARK_GRAY, + textPrimaryOnBackground: Color.WHITE, + textSecondaryOnBackground: Color.LIGHT_GRAY, + + navBar: Color.DARK_GRAY, + navBarBorder: Color.DARK_GRAY, // no visible border + + primary: Color.WHITE, + onPrimary: Color.DARK_GRAY, + + disabled: '#A9AFBA', +}; + +const THEME_MAP = { + default: defaultTheme, + violet, + charcoal, +}; + +/** A view which uses the theme's background color */ +export const ThemedBackground = styled.View` + background-color: ${({ theme }) => theme.background}; +`; + +/** + * Return theme config for a known theme name + * + * @param {'default' | 'violet' | 'charcoal'} themeName + */ +export const getTheme = themeName => { + const themeColors = THEME_MAP[themeName]; + if (!themeColors) { + throw new Error(`Unknown theme ${themeName}`); + } + return themeColors; +}; + +/** + * Configures themeable sub-components with background, text (primary, + * secondary), button and other various theme colors. + * + * Usage: + * + * This will render a charcoal view, with a white heading, and a grey paragraph. + * + * ```jsx + * + * Test + * Paragraph here + * + * ``` + * + * It makes available the following `props.theme.x` properties in styled components: + * + * - background - the standard bg color + * - primary - the primary theme/button color on top of bakground (e.g. violet) + * - onPrimary - color to show on top of primary (usually white) + * - onPrimary - color to show on top of primary (usually white) + * - textPrimaryOnBackground - text color to show on background + * - textSecondaryOnBackground - secondary text color to show on background + * - warning - warning color (always orange) + * - success - success color (always green) + * - border - border/divider color (usually gray) + * + * @param {{ + * use?: string, + * setBackground?: boolean, + * }} param0 + */ +export const Theme = ({ use = 'default', setBackground = false, children }) => { + return ( + + {setBackground ? ( + {children} + ) : ( + children + )} + + ); +}; diff --git a/app/locales/all-dayjs-locales.js b/app/locales/all-dayjs-locales.js new file mode 100644 index 0000000000..68c9a014bb --- /dev/null +++ b/app/locales/all-dayjs-locales.js @@ -0,0 +1,133 @@ +import 'dayjs/locale/af'; +import 'dayjs/locale/ar-dz'; +import 'dayjs/locale/ar-kw'; +import 'dayjs/locale/ar-ly'; +import 'dayjs/locale/ar-ma'; +import 'dayjs/locale/ar-sa'; +import 'dayjs/locale/ar-tn'; +import 'dayjs/locale/ar'; +import 'dayjs/locale/az'; +import 'dayjs/locale/be'; +import 'dayjs/locale/bg'; +import 'dayjs/locale/bi'; +import 'dayjs/locale/bm'; +import 'dayjs/locale/bn'; +import 'dayjs/locale/bo'; +import 'dayjs/locale/br'; +import 'dayjs/locale/bs'; +import 'dayjs/locale/ca'; +import 'dayjs/locale/cs'; +import 'dayjs/locale/cv'; +import 'dayjs/locale/cy'; +import 'dayjs/locale/da'; +import 'dayjs/locale/de-at'; +import 'dayjs/locale/de-ch'; +import 'dayjs/locale/de'; +import 'dayjs/locale/dv'; +import 'dayjs/locale/el'; +import 'dayjs/locale/en-SG'; +import 'dayjs/locale/en-au'; +import 'dayjs/locale/en-ca'; +import 'dayjs/locale/en-gb'; +import 'dayjs/locale/en-ie'; +import 'dayjs/locale/en-il'; +import 'dayjs/locale/en-in'; +import 'dayjs/locale/en-nz'; +import 'dayjs/locale/en-tt'; +import 'dayjs/locale/en'; +import 'dayjs/locale/eo'; +import 'dayjs/locale/es-do'; +import 'dayjs/locale/es-us'; +import 'dayjs/locale/es'; +import 'dayjs/locale/et'; +import 'dayjs/locale/eu'; +import 'dayjs/locale/fa'; +import 'dayjs/locale/fi'; +import 'dayjs/locale/fo'; +import 'dayjs/locale/fr-ca'; +import 'dayjs/locale/fr-ch'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/fy'; +import 'dayjs/locale/ga'; +import 'dayjs/locale/gd'; +import 'dayjs/locale/gl'; +import 'dayjs/locale/gom-latn'; +import 'dayjs/locale/gu'; +import 'dayjs/locale/he'; +import 'dayjs/locale/hi'; +import 'dayjs/locale/hr'; +import 'dayjs/locale/hu'; +import 'dayjs/locale/hy-am'; +import 'dayjs/locale/id'; +import 'dayjs/locale/is'; +import 'dayjs/locale/it-ch'; +import 'dayjs/locale/it'; +import 'dayjs/locale/ja'; +import 'dayjs/locale/jv'; +import 'dayjs/locale/ka'; +import 'dayjs/locale/kk'; +import 'dayjs/locale/km'; +import 'dayjs/locale/kn'; +import 'dayjs/locale/ko'; +import 'dayjs/locale/ku'; +import 'dayjs/locale/ky'; +import 'dayjs/locale/lb'; +import 'dayjs/locale/lo'; +import 'dayjs/locale/lt'; +import 'dayjs/locale/lv'; +import 'dayjs/locale/me'; +import 'dayjs/locale/mi'; +import 'dayjs/locale/mk'; +import 'dayjs/locale/ml'; +import 'dayjs/locale/mn'; +import 'dayjs/locale/mr'; +import 'dayjs/locale/ms-my'; +import 'dayjs/locale/ms'; +import 'dayjs/locale/mt'; +import 'dayjs/locale/my'; +import 'dayjs/locale/nb'; +import 'dayjs/locale/ne'; +import 'dayjs/locale/nl-be'; +import 'dayjs/locale/nl'; +import 'dayjs/locale/nn'; +import 'dayjs/locale/oc-lnc'; +import 'dayjs/locale/pa-in'; +import 'dayjs/locale/pl'; +import 'dayjs/locale/pt-br'; +import 'dayjs/locale/pt'; +import 'dayjs/locale/ro'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/sd'; +import 'dayjs/locale/se'; +import 'dayjs/locale/si'; +import 'dayjs/locale/sk'; +import 'dayjs/locale/sl'; +import 'dayjs/locale/sq'; +import 'dayjs/locale/sr-cyrl'; +import 'dayjs/locale/sr'; +import 'dayjs/locale/ss'; +import 'dayjs/locale/sv'; +import 'dayjs/locale/sw'; +import 'dayjs/locale/ta'; +import 'dayjs/locale/te'; +import 'dayjs/locale/tet'; +import 'dayjs/locale/tg'; +import 'dayjs/locale/th'; +import 'dayjs/locale/tl-ph'; +import 'dayjs/locale/tlh'; +import 'dayjs/locale/tr'; +import 'dayjs/locale/tzl'; +import 'dayjs/locale/tzm-latn'; +import 'dayjs/locale/tzm'; +import 'dayjs/locale/ug-cn'; +import 'dayjs/locale/uk'; +import 'dayjs/locale/ur'; +import 'dayjs/locale/uz-latn'; +import 'dayjs/locale/uz'; +import 'dayjs/locale/vi'; +import 'dayjs/locale/x-pseudo'; +import 'dayjs/locale/yo'; +import 'dayjs/locale/zh-cn'; +import 'dayjs/locale/zh-hk'; +import 'dayjs/locale/zh-tw'; +import 'dayjs/locale/zh'; diff --git a/app/locales/en.json b/app/locales/en.json index 8896efe9ff..30ce2c4e2e 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -28,7 +28,6 @@ "home_mayo_link_heading": "More COVID-19 information", "home_mayo_link_label": "from the Mayo Clinic", "home_mayo_link_URL": "https://www.mayoclinic.org/coronavirus-covid-19", - "home_next_steps": "Find out more", "home_no_contact_header": "No known contact", "home_no_contact_subtext": "Based on available data you haven’t been near anyone reported positive for COVID-19.", "home_unknown_header": "Unknown", @@ -75,7 +74,7 @@ "no_data": "No data", "notification_2_weeks_ago": "2 weeks ago", "notification_data_not_available": "No exposure data is available.", - "notification_random_data_button": "Select Healthcare Authority", + "notification_select_authority": "Select Healthcare Authority", "notification_title": "2-Week Exposure Profile", "notification_today": "today", "notification_warning_text": "If a Healthcare Authority exists in your area you can subscribe to obtain regular updates of exposure risks.", @@ -89,6 +88,7 @@ "overlap_title": "Check Overlap", "push_at_risk_message": "You have crossed paths with a COVID-19 patient", "push_at_risk_title": "You may be at risk", + "see_exposure_history": "See exposure history", "settings_title": "Dashboard", "share_location_data": "Share location data", "show_overlap": "Click to view the public dataset", @@ -98,5 +98,15 @@ "terms_of_use_url": "https://docs.google.com/document/d/1mtdal_pywsKZVMXLHjjj5eKznipPLP8sM1HwFTIhjo0/edit#", "tested_positive_subtitle": "Your private data can be transferred to health authorities, backed up, or otherwise shared.", "tested_positive_title": "Share location history" + }, + "history": { + "no_exposure": "No exposure", + "possible_exposure_para": "It is possible you were in contact with or close to someone who tested positive for COVID-19", + "possible_exposure": "Possible exposure", + "timeline": "Timeline", + "what_does_this_mean_para": "Based on your GPS history, it is possible that you may have been in contact with or close to somebody who was diagnosed with COVID19. This does not mean you are infected but that you might be.\n\nFor further information on what you should do you can refer to the Mayo Clinic’s website.", + "what_does_this_mean": "What does this mean?", + "what_if_no_symptoms_para": "If you have no symptoms but still would like to be tested you can go to your nearest testing site.\n\nIndividuals who don't exhibit symptoms can sometimes still carrythe infection and infect others. Being careful about social distancing and coming in contact with large groups or at risk individuals (the elderly, those with significant other medical issues) is important to manage both your risk and the risk to others.", + "what_if_no_symptoms": "What if I’m not showing symptoms?" } } \ No newline at end of file diff --git a/app/locales/languages.js b/app/locales/languages.js index a012a6e623..b64799e994 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -1,4 +1,8 @@ +import './all-dayjs-locales'; + +import dayjs from 'dayjs'; import i18next from 'i18next'; +import { NativeModules, Platform } from 'react-native'; import { getLanguages } from 'react-native-i18n'; import { LANG_OVERRIDE } from '../constants/storage'; @@ -22,6 +26,14 @@ import zh_Hant from './zh-Hant.json'; // 5. REMOVE all empty translations. e.g. "key": "", this will allow fallback to the default: English // 6. import xyIndex from `./xy.json` and add the language to the block at the bottom +const deviceLocale = + Platform.OS === 'ios' + ? NativeModules.SettingsManager.settings.AppleLocale || // iOS < 13 + NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13 + : NativeModules.I18nManager.localeIdentifier; // Android + +dayjs.locale([deviceLocale, 'en']); + // This will fetch the user's language // Set up as a function so first onboarding screen can also update // ...from async language override setting @@ -37,8 +49,10 @@ export function findUserLang(callback) { console.log(res); userLang = res; i18next.changeLanguage(res); + dayjs.locale(res); } else { i18next.changeLanguage(userLang); + dayjs.locale(userLang); } // Run state updating callback to trigger rerender diff --git a/app/views/ExposureHistory/CalendarDay.js b/app/views/ExposureHistory/CalendarDay.js new file mode 100644 index 0000000000..66036a9497 --- /dev/null +++ b/app/views/ExposureHistory/CalendarDay.js @@ -0,0 +1,65 @@ +import styled from '@emotion/native'; +import dayjs from 'dayjs'; +import React from 'react'; + +import { DataCircle, Risk } from './DataCircle'; + +/** + * Show a circular calendar day with risk level coloring. + * + * @param {{ + * date: dayjs.Dayjs, + * exposureMinutes?: number, + * showMonthLabel?: boolean, + * highlightIfToday?: boolean, + * }} param0 + */ +export const CalendarDay = ({ + date, + exposureMinutes, + highlightIfToday = false, + showMonthLabel = false, +}) => { + let riskLevel = Risk.Unknown; + + const today = dayjs(); + if (highlightIfToday && today.isSame(date, 'day')) { + riskLevel = Risk.Today; + } + + // No risk is less important than "today" + if (exposureMinutes === 0) { + riskLevel = Risk.None; + } + + // possible risk is more important than "today" + if (exposureMinutes > 0) { + riskLevel = Risk.Possible; + } + + return ( + + {showMonthLabel && {date.format('MMM')}} + + {date.format('D')} + + + ); +}; + +const DayContainer = styled.View` + align-items: center; + padding: 4px; +`; + +export const DayOfWeek = styled.Text` + color: ${({ theme }) => theme.textPrimaryOnBackground}; + font-family: 'IBM Plex Sans'; + font-size: 9px; + font-weight: bold; + letter-spacing: 1.5px; + line-height: 20px; + line-height: 20px; + text-align: center; + text-transform: uppercase; +`; diff --git a/app/views/ExposureHistory/DataCircle.js b/app/views/ExposureHistory/DataCircle.js new file mode 100644 index 0000000000..eff72484f0 --- /dev/null +++ b/app/views/ExposureHistory/DataCircle.js @@ -0,0 +1,87 @@ +import styled from '@emotion/native'; +import React from 'react'; + +export const Risk = { + Unknown: 'unknown', + None: 'none', + Possible: 'possible', + Today: 'today', +}; + +/** + * + * @param {{ + * riskLevel: string, + * size: number + * }} param0 + */ +export const DataCircle = ({ riskLevel, children, size = 36 }) => { + return ( + + {children} + + ); +}; + +/** Get bg color from risk level and theme warning/success */ +export const getBgColor = ({ riskLevel, theme }) => { + if (riskLevel === Risk.Possible) { + return theme.warning; + } + + if (riskLevel === Risk.None) { + return theme.success; + } + + return 'transparent'; +}; + +/** Get border color from theme and risk level */ +const getBorderColor = ({ riskLevel, theme }) => { + let color = 'transparent'; + + if (riskLevel === Risk.Unknown) { + color = theme.disabled; + } + + if (riskLevel === Risk.Today) { + color = theme.textPrimaryOnBackground; + } + + return color; +}; + +const getSize = ({ size }) => `${size}px`; +const getBorderRadius = ({ size }) => `${size / 2}px`; + +const Circle = styled.View` + background-color: ${getBgColor}; + border: 1px solid ${getBorderColor}; + width: ${getSize}; + height: ${getSize}; + border-radius: ${getBorderRadius}; + align-items: center; + align-content: center; + flex-direction: row; +`; + +/** Get border color from theme and risk level */ +const getTextColor = ({ riskLevel, theme }) => { + if (riskLevel === Risk.Unknown) { + return theme.disabled; + } + + if (riskLevel === Risk.None || riskLevel === Risk.Possible) { + return theme.onPrimary; + } + + return theme.textPrimaryOnBackground; +}; + +const CircleInnerText = styled.Text` + color: ${getTextColor}; + text-align: center; + flex: 1; + font-size: 12px; + font-weight: bold; +`; diff --git a/app/views/ExposureHistory/DetailedHistory.js b/app/views/ExposureHistory/DetailedHistory.js new file mode 100644 index 0000000000..35042d864f --- /dev/null +++ b/app/views/ExposureHistory/DetailedHistory.js @@ -0,0 +1,67 @@ +import styled from '@emotion/native'; +import React from 'react'; + +import { Typography } from '../../components/Typography'; +import languages from '../../locales/languages'; +import { ExposureCalendarView } from './ExposureCalendarView'; +import { SingleExposureDetail } from './SingleExposureDetail'; + +/** + * Detailed info when there is some exposure found + * + * @param {{history: !import('../../constants/history').History}} param0 + */ +export const DetailedHistory = ({ history }) => { + const exposedDays = history.filter(day => day.exposureMinutes > 0); + return ( + <> + + + + + {exposedDays.map(({ exposureMinutes, date }) => ( + + ))} + + {exposedDays.length === 0 ? ( + <> + + {languages.t('label.home_no_contact_header')} + + + {languages.t('label.home_no_contact_subtext')} + + + + ) : null} + + + {languages.t('history.what_does_this_mean')} + + + {languages.t('history.what_does_this_mean_para')} + + + + {exposedDays.length ? ( + <> + + {languages.t('history.what_if_no_symptoms')} + + + {languages.t('history.what_if_no_symptoms_para')} + + + ) : null} + + ); +}; + +const Divider = styled.View` + height: 24px; + width: 100%; +`; diff --git a/app/views/ExposureHistory/ExposureCalendarView.js b/app/views/ExposureHistory/ExposureCalendarView.js new file mode 100644 index 0000000000..96d6322e71 --- /dev/null +++ b/app/views/ExposureHistory/ExposureCalendarView.js @@ -0,0 +1,99 @@ +import styled, { css } from '@emotion/native'; +import dayjs from 'dayjs'; +import React from 'react'; + +import { Typography } from '../../components/Typography'; +import languages from '../../locales/languages'; +import { CalendarDay, DayOfWeek } from './CalendarDay'; +import { DataCircle, Risk } from './DataCircle'; +import { MonthGrid } from './MonthGrid'; + +/** + * Get a date key that does not include time. + * + * @param {import('dayjs').Dayjs} date + */ +function getDayKey(date) { + return date.startOf('day').unix(); +} + +/** + * Calendar view of recent exposures. + * + * @param {{ + * history: !import('../../constants/history').HistoryDay[], + * weeks?: number, + * }} param0 + */ +export const ExposureCalendarView = ({ history, weeks }) => { + /** @type {{[date: string]: number}} */ + const exposureMap = {}; + history.forEach(day => { + exposureMap[getDayKey(day.date)] = day.exposureMinutes; + }); + + const lastMonth = dayjs().subtract(1, 'month'); + + // TODO: this could do with better i18n + const title = `${lastMonth.format('MMMM')}/${dayjs().format('MMMM')}`; + + return ( + <> + {title} + {d}} + renderDay={date => { + const exposureMinutes = exposureMap[getDayKey(date)]; + return ( + + ); + }} + /> + + + {languages.t('history.no_exposure')} + + + {languages.t('history.possible_exposure')} + + + {languages.t('label.no_data')} + + + + ); +}; + +const LegendRow = styled.View` + flex-direction: row; + justify-content: center; + margin-top: 16px; +`; + +const LegendItem = ({ children, riskLevel }) => { + return ( + + + + {children} + + + ); +}; + +const LegendItemContainer = styled.View` + flex-direction: row; + align-items: center; + margin-right: 16px; + margin-left: 8px; +`; diff --git a/app/views/ExposureHistory/ExposureHistory.js b/app/views/ExposureHistory/ExposureHistory.js new file mode 100644 index 0000000000..c9dcc705fe --- /dev/null +++ b/app/views/ExposureHistory/ExposureHistory.js @@ -0,0 +1,133 @@ +import { css } from '@emotion/native'; +import dayjs from 'dayjs'; +import React, { useEffect, useState } from 'react'; +import { BackHandler, ScrollView } from 'react-native'; + +import NavigationBarWrapper from '../../components/NavigationBarWrapper'; +import { Typography } from '../../components/Typography'; +import { BIN_DURATION, MAX_EXPOSURE_WINDOW } from '../../constants/history'; +import { CROSSED_PATHS } from '../../constants/storage'; +import { Theme, charcoal, defaultTheme } from '../../constants/themes'; +import { GetStoreData } from '../../helpers/General'; +import languages from '../../locales/languages'; +import { DetailedHistory } from './DetailedHistory'; + +const NO_HISTORY = []; + +export const ExposureHistoryScreen = ({ navigation }) => { + const [history, setHistory] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + let dayBins = await GetStoreData(CROSSED_PATHS); + + // dayBins = generateFakeIntersections(1); // handy for creating faux data + + if (dayBins === null) { + setHistory(NO_HISTORY); + console.log("Can't find Crossed Paths"); + } else { + console.log('Found Crossed Paths'); + setHistory(convertToDailyMinutesExposed(dayBins)); + } + setIsLoading(false); + } + + fetchData(); + + BackHandler.addEventListener('hardwareBackPress', handleBackPress); + + // teardown code + return () => { + BackHandler.removeEventListener('hardwareBackPress', handleBackPress); + }; + }, []); + + const handleBackPress = () => { + navigation.goBack(); + return true; + }; + + const hasExposure = + history?.length && history.some(h => h.exposureMinutes > 0); + + const themeBackground = hasExposure + ? charcoal.background + : defaultTheme.background; + + const themeText = hasExposure + ? charcoal.textPrimaryOnBackground + : defaultTheme.textPrimaryOnBackground; + + return ( + + navigation.goBack()}> + + {isLoading ? ( + + {languages.t('label.loading_public_data')} + + ) : ( + + )} + + + + ); +}; + +// eslint-disable-next-line no-unused-vars +function generateFakeIntersections(days = MAX_EXPOSURE_WINDOW, maxBins = 50) { + let pseudoBin = []; + for (let i = 0; i < days; i++) { + // Random Integer between 0-99 + const intersections = Math.max( + Math.floor(Math.random() * maxBins - maxBins / 2), + 0, + ); + pseudoBin.push(intersections); + } + return JSON.stringify(pseudoBin); +} + +/** + * Convert the daily "bins" payload to an array of daily minutes of exposure. + * + * e.g. + * + * ```js + * "[1,2,3]" // bins exposed + * [{date: Date, exposureMinutes: 5}, ...] // minutes exposed + * ``` + * + * @param {string} dayBin JSON stringified array of numbers e.g. `"[1,2,3]"` + * @returns {import('../../constants/history').History} Array of exposed minutes per day starting at today + */ +export function convertToDailyMinutesExposed(dayBin) { + let dayBinParsed = JSON.parse(dayBin); + + if (!dayBinParsed) { + console.log('Crossed Paths is null'); + return NO_HISTORY; + } + + const today = dayjs(); + + const dailyMinutesExposed = dayBinParsed + .slice(0, MAX_EXPOSURE_WINDOW) // last two weeks of crossing data only + .map((binCount, i) => { + return { + date: today.startOf('day').subtract(i, 'day'), + exposureMinutes: binCount * BIN_DURATION, + }; + }); + return dailyMinutesExposed; +} diff --git a/app/views/ExposureHistory/MonthGrid.js b/app/views/ExposureHistory/MonthGrid.js new file mode 100644 index 0000000000..ebd7f106c2 --- /dev/null +++ b/app/views/ExposureHistory/MonthGrid.js @@ -0,0 +1,96 @@ +import styled from '@emotion/native'; +import dayjs from 'dayjs'; +import localeData from 'dayjs/plugin/localeData'; +import React from 'react'; + +dayjs.extend(localeData); + +/** + * Get an array of daily dates between start and end. + * + * @param {dayjs.Dayjs} start + * @param {dayjs.Dayjs} end + * @returns {dayjs.Dayjs[]} + */ +export function daysBetween(start, end, interval = 'day') { + const days = []; + let d = start; + while (d.isBefore(end, interval) || d.isSame(end, interval)) { + days.push(d); + d = d.add(1, interval); + } + return days; +} + +/** + * Render a grid of days grouped by rows of n weeks. + * + * @param {{ + * date?: dayjs.Dayjs, + * weeks?: number, + * renderDayHeader: (d: string) => import('react').ReactNode + * renderDay: (d: dayjs.Dayjs) => import('react').ReactNode + * }} param0 + */ +export const MonthGrid = ({ + date = dayjs(), + weeks = 3, + renderDayHeader, + renderDay, +}) => { + if (!renderDay || !renderDayHeader) { + throw new Error('renderDay and renderDayHeader are required'); + } + + const headers = dayjs.localeData().weekdaysShort(); // 'mon', 'tue', ... + + const start = date + .startOf('day') + .startOf('week') + .subtract(weeks - 1, 'week'); + + const end = date.startOf('day').endOf('week'); + + /** @type {dayjs.Dayjs[]} */ + const days = daysBetween(start, end, 'day'); + + return ( + + + {headers.map(day => ( + {renderDayHeader(day)} + ))} + + + {days.map(day => ( + {renderDay(day)} + ))} + + + ); +}; + +const Container = styled.View` + margin: 0 -8px; // offsets some of the percentage based cell padding +`; + +const HeaderRow = styled.View` + flex-direction: row; +`; + +const DAYS_PER_WEEK = 7; + +const CELL_WIDTH_PERCENT = (100 / DAYS_PER_WEEK).toFixed(0); + +const DateGrid = styled.View` + flex-direction: row; + flex-wrap: wrap; +`; + +const CellWrapper = styled.View` + flex: 1; + flex-basis: ${CELL_WIDTH_PERCENT}%; + align-items: center; + justify-content: center; + min-height: 20px; // for the header text +`; diff --git a/app/views/ExposureHistory/SingleExposureDetail.js b/app/views/ExposureHistory/SingleExposureDetail.js new file mode 100644 index 0000000000..b7b02916e0 --- /dev/null +++ b/app/views/ExposureHistory/SingleExposureDetail.js @@ -0,0 +1,96 @@ +import dayjs from 'dayjs'; +import React from 'react'; +import styled from '@emotion/native'; +import { CalendarDay } from './CalendarDay'; +import languages from '../../locales/languages'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; + +dayjs.extend(relativeTime); +dayjs.extend(duration); +dayjs.extend(localizedFormat); + +const MINUTE_IN_MS = 60 * 1000; + +/** + * Details for a single day exposure. + * + * @param {!import('../../constants/history').HistoryDay} props + */ +export const SingleExposureDetail = ({ date, exposureMinutes }) => { + const exposureTimeHumanized = dayjs + .duration(exposureMinutes * MINUTE_IN_MS) + .humanize(); + + // TODO: display today, yesterday, calendar() does not quite cut it. + const dateHumanized = date.format('ll'); + + return ( + + + + {languages.t('history.possible_exposure')} + + {exposureTimeHumanized} + + {dateHumanized} + + {languages.t('history.possible_exposure_para')} + + + ); +}; + +const Container = styled.View` + align-items: stretch; + flex-direction: row; + margin-bottom: 20px; + width: 100%; +`; + +const DetailsBox = styled.View` + border-radius: 6px; + border: 1px solid ${({ theme }) => theme.warning}; + flex: 1; + margin-left: 14px; + padding: 16px 16px 24px 16px; +`; + +const Heading = styled.Text` + color: ${({ theme }) => theme.warning}; + font-size: 16px; + font-weight: bold; + line-height: 20px; +`; + +const SubheadingContainer = styled.View` + align-items: center; + flex-direction: row; + margin-bottom: 14px; +`; + +const Divider = styled.View` + background-color: ${({ theme }) => theme.textPrimaryOnBackground}; + border-radius: 1px; + height: 3px; + margin: 0 12px; + width: 3px; +`; + +const BodyText = styled.Text` + color: ${({ theme }) => theme.textSecondaryOnBackground}; + font-size: 14px; + line-height: 20px; +`; + +const SubheadingText = styled(BodyText)` + color: ${({ theme }) => theme.textPrimaryOnBackground}; + font-size: 14px; + font-weight: bold; + line-height: 20px; +`; diff --git a/app/views/ExposureHistory/__tests__/CalendarDay.spec.js b/app/views/ExposureHistory/__tests__/CalendarDay.spec.js new file mode 100644 index 0000000000..e27feddd11 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/CalendarDay.spec.js @@ -0,0 +1,77 @@ +import { render } from '@testing-library/react-native'; +import dayjs from 'dayjs'; +import MockDate from 'mockdate'; +import React from 'react'; + +import { Theme } from '../../../constants/themes'; +import { CalendarDay } from '../CalendarDay'; + +const FIXED_DATE = dayjs('2020-01-09T00:00:00-08:00').startOf('day'); + +beforeEach(() => { + MockDate.set(FIXED_DATE.valueOf()); +}); + +afterEach(() => { + MockDate.reset(); +}); + +it('possible exposure matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('no exposure matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('unknown exposure matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('today matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('no risk takes prio over today', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('some risk takes prio over today', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); diff --git a/app/views/ExposureHistory/__tests__/DataCircle.spec.js b/app/views/ExposureHistory/__tests__/DataCircle.spec.js new file mode 100644 index 0000000000..0fa7ba1471 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/DataCircle.spec.js @@ -0,0 +1,55 @@ +import { render } from '@testing-library/react-native'; +import React from 'react'; + +import { Theme } from '../../../constants/themes'; +import { DataCircle, Risk } from '../DataCircle'; + +it('possible exposure matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('no exposure matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('unknown exposure matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('today matches snapshot', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('size is configurable (snapshot)', () => { + const { asJSON } = render( + + + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); diff --git a/app/views/ExposureHistory/__tests__/ExposureHistory.spec.js b/app/views/ExposureHistory/__tests__/ExposureHistory.spec.js new file mode 100644 index 0000000000..f04399d5e2 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/ExposureHistory.spec.js @@ -0,0 +1,48 @@ +import dayjs from 'dayjs'; +import MockDate from 'mockdate'; + +import { convertToDailyMinutesExposed } from '../ExposureHistory'; + +describe('convertToDailyMinutesExposed', () => { + const NOW = dayjs('2020-01-09T00:00:00-08:00'); + + beforeEach(() => { + MockDate.set(NOW.valueOf()); + }); + + afterEach(() => { + MockDate.reset(); + }); + + it('returns empty array if there is null history', () => { + expect(convertToDailyMinutesExposed('null')).toEqual([]); + }); + + it('returns empty array if there is empty day bins', () => { + expect(convertToDailyMinutesExposed('[]')).toEqual([]); + }); + + it('converts day bins to minutes', () => { + expect(convertToDailyMinutesExposed('[0, 1, 2, 3, 0]')).toEqual([ + { date: expect.any(dayjs), exposureMinutes: 0 }, + { date: expect.any(dayjs), exposureMinutes: 5 }, + { date: expect.any(dayjs), exposureMinutes: 10 }, + { date: expect.any(dayjs), exposureMinutes: 15 }, + { date: expect.any(dayjs), exposureMinutes: 0 }, + ]); + }); + + it('converts `daysAgo` index to daily date objects', () => { + const day = convertToDailyMinutesExposed('[1, 1, 0]'); + + const TODAY = NOW.startOf('day'); + + expect(day[0].date.startOf('day').format()).toBe(TODAY.format()); + expect(day[1].date.startOf('day').format()).toBe( + TODAY.subtract(1, 'day').format(), + ); + expect(day[2].date.startOf('day').format()).toBe( + TODAY.subtract(2, 'day').format(), + ); + }); +}); diff --git a/app/views/ExposureHistory/__tests__/MonthGrid.spec.js b/app/views/ExposureHistory/__tests__/MonthGrid.spec.js new file mode 100644 index 0000000000..b7853c3f65 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/MonthGrid.spec.js @@ -0,0 +1,52 @@ +import { render } from '@testing-library/react-native'; +import dayjs from 'dayjs'; +import MockDate from 'mockdate'; +import React from 'react'; + +import { MonthGrid } from '../MonthGrid'; + +const NOW = dayjs('2020-01-09T00:00:00-08:00').startOf('day'); + +const renderDayHeader = dayOfWeek => {dayOfWeek}; +const renderDay = date => {date.format('D')}; + +const renderProps = { renderDayHeader, renderDay }; + +beforeEach(() => { + MockDate.set(NOW.valueOf()); +}); + +afterEach(() => { + MockDate.reset(); +}); + +it('renderDay is required', () => { + jest.spyOn(console, 'error').mockImplementation(); + expect(() => + render(), + ).toThrow(); +}); + +it('renderDayHeader is required', () => { + jest.spyOn(console, 'error').mockImplementation(); + expect(() => render()).toThrow(); +}); + +it('with no input date, defaults to 3 weeks ago from today', () => { + const { asJSON } = render(); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('with input date, defaults to 3 weeks ago from that date', () => { + const date = dayjs('2019-12-02').subtract(); + const { asJSON } = render(); + + expect(asJSON()).toMatchSnapshot(); +}); + +it('allows specifying number of weeks', () => { + const { asJSON } = render(); + + expect(asJSON()).toMatchSnapshot(); +}); diff --git a/app/views/ExposureHistory/__tests__/SingleExposureDetails.spec.js b/app/views/ExposureHistory/__tests__/SingleExposureDetails.spec.js new file mode 100644 index 0000000000..5dbe102c98 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/SingleExposureDetails.spec.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import dayjs from 'dayjs'; + +import { SingleExposureDetail } from '../SingleExposureDetail'; + +const FIXED_DATE = dayjs('2020-04-11').startOf('day'); + +it('matches snapshot', () => { + const { asJSON } = render( + , + ); + + expect(asJSON()).toMatchSnapshot(); +}); diff --git a/app/views/ExposureHistory/__tests__/__snapshots__/CalendarDay.spec.js.snap b/app/views/ExposureHistory/__tests__/__snapshots__/CalendarDay.spec.js.snap new file mode 100644 index 0000000000..7564b3d558 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/__snapshots__/CalendarDay.spec.js.snap @@ -0,0 +1,379 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`no exposure matches snapshot 1`] = ` + + + + + 9 + + + + +`; + +exports[`no risk takes prio over today 1`] = ` + + + + + 9 + + + + +`; + +exports[`possible exposure matches snapshot 1`] = ` + + + + + 9 + + + + +`; + +exports[`some risk takes prio over today 1`] = ` + + + + + 9 + + + + +`; + +exports[`today matches snapshot 1`] = ` + + + + + 9 + + + + +`; + +exports[`unknown exposure matches snapshot 1`] = ` + + + + + 9 + + + + +`; diff --git a/app/views/ExposureHistory/__tests__/__snapshots__/DataCircle.spec.js.snap b/app/views/ExposureHistory/__tests__/__snapshots__/DataCircle.spec.js.snap new file mode 100644 index 0000000000..5d27b74c96 --- /dev/null +++ b/app/views/ExposureHistory/__tests__/__snapshots__/DataCircle.spec.js.snap @@ -0,0 +1,246 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`no exposure matches snapshot 1`] = ` + + + + + +`; + +exports[`possible exposure matches snapshot 1`] = ` + + + + + +`; + +exports[`size is configurable (snapshot) 1`] = ` + + + + + +`; + +exports[`today matches snapshot 1`] = ` + + + + + +`; + +exports[`unknown exposure matches snapshot 1`] = ` + + + + + +`; diff --git a/app/views/ExposureHistory/__tests__/__snapshots__/MonthGrid.spec.js.snap b/app/views/ExposureHistory/__tests__/__snapshots__/MonthGrid.spec.js.snap new file mode 100644 index 0000000000..f289f59c0c --- /dev/null +++ b/app/views/ExposureHistory/__tests__/__snapshots__/MonthGrid.spec.js.snap @@ -0,0 +1,907 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`allows specifying number of weeks 1`] = ` + + + + + + Sun + + + + + Mon + + + + + Tue + + + + + Wed + + + + + Thu + + + + + Fri + + + + + Sat + + + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + +`; + +exports[`with input date, defaults to 3 weeks ago from that date 1`] = ` + + + + + + Sun + + + + + Mon + + + + + Tue + + + + + Wed + + + + + Thu + + + + + Fri + + + + + Sat + + + + + + +`; + +exports[`with no input date, defaults to 3 weeks ago from today 1`] = ` + + + + + + Sun + + + + + Mon + + + + + Tue + + + + + Wed + + + + + Thu + + + + + Fri + + + + + Sat + + + + + + + 22 + + + + + 23 + + + + + 24 + + + + + 25 + + + + + 26 + + + + + 27 + + + + + 28 + + + + + 29 + + + + + 30 + + + + + 31 + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + +`; diff --git a/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap b/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap new file mode 100644 index 0000000000..62a93e96ef --- /dev/null +++ b/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap @@ -0,0 +1,179 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshot 1`] = ` + + + + + Apr + + + + 11 + + + + + + Possible exposure + + + + 5 minutes + + + + Apr 11, 2020 + + + + It is possible you were in contact with or close to someone who tested positive for COVID-19 + + + + +`; diff --git a/app/views/LocationTracking.js b/app/views/LocationTracking.js index 55ca967257..519185b141 100644 --- a/app/views/LocationTracking.js +++ b/app/views/LocationTracking.js @@ -377,9 +377,9 @@ class LocationTracking extends Component { // }; return; } else if (this.state.currentState === StateEnum.AT_RISK) { - buttonLabel = languages.t('label.home_next_steps'); + buttonLabel = languages.t('label.see_exposure_history'); buttonFunction = () => { - this.props.navigation.navigate('NotificationScreen'); + this.props.navigation.navigate('ExposureHistoryScreen'); }; } else if (this.state.currentState === StateEnum.UNKNOWN) { buttonLabel = languages.t('label.home_enable_location'); diff --git a/app/views/Notification.js b/app/views/Notification.js deleted file mode 100644 index 1a725bf1bb..0000000000 --- a/app/views/Notification.js +++ /dev/null @@ -1,359 +0,0 @@ -import React, { Component } from 'react'; -import { - BackHandler, - Dimensions, - ScrollView, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native'; -import { VictoryAxis, VictoryBar, VictoryChart } from 'victory-native'; - -import languages from './../locales/languages'; -import NavigationBarWrapper from '../components/NavigationBarWrapper'; -import colors from '../constants/colors'; -import Colors from '../constants/colors'; -import fontFamily from '../constants/fonts'; -import { CROSSED_PATHS } from '../constants/storage'; -import { GetStoreData } from '../helpers/General'; - -const width = Dimensions.get('window').width; -const height = Dimensions.get('window').height; -const max_exposure_window = 14; // Two weeks is the longest view that matters -const bin_duration = 5; // each bin count represents a 5 minute period - -// Thresholds are the number of counts in the intersection bins (5 minute -// timeframse) which are used to provide user interface hints. -// TODO: These thresholds semi-arbitrary, this could use some medical research. -const Threshold = { - HIGH: 96, // 96 * 5 minutes = eight hours - MEDIUM: 36, // 36 * 5 minutes = three hours - LOW: 12, // 12 * 5 minutes = one hour - LOWEST: 0, // 0 * 5 minutes = no known exposure! -}; - -class NotificationScreen extends Component { - constructor(props) { - super(props); - this.state = { - data: [], - }; - this.getInitialState(); - } - - backToMain() { - this.resetState(); - this.props.navigation.goBack(); - } - - goToSettings() { - this.resetState(); - this.props.navigation.navigate('ChooseProviderScreen', {}); - } - - handleBackPress = () => { - this.resetState(); - this.props.navigation.goBack(); - return true; - }; - - componentDidMount() { - this.refreshState(); - BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); - } - - componentWillUnmount() { - BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); - } - - refreshState() { - this.getInitialState(); - } - - resetState() {} - - getInitialState = async () => { - GetStoreData(CROSSED_PATHS).then(dayBin => { - console.log(dayBin); - - /* DEBUGGING TOOL -- handy for creating faux data - let pseudoBin = []; - for (var i = 0; i < 28; i++) { - // Random Integer between 0-99 - const intersections = Math.floor((Math.random() * 500) / 300); - pseudoBin.push(intersections); - } - dayBin = JSON.stringify(pseudoBin); - */ - - if (dayBin === null) { - this.setState({ dataAvailable: false }); - console.log("Can't found Crossed Paths"); - } else { - var crossed_path_data = []; - console.log('Found Crossed Paths'); - this.setState({ dataAvailable: true }); - let dayBinParsed = JSON.parse(dayBin); - - // Don't display more than two weeks of crossing data - for ( - var i = 0; - i < dayBinParsed.length && i < max_exposure_window; - i++ - ) { - const val = dayBinParsed[i]; - let data = { x: i, y: val, fill: this.colorfill(val) }; - crossed_path_data.push(data); - } - this.setState({ data: crossed_path_data }); - } - }); - }; - - colorfill(data) { - var color = colors.LOWEST_RISK; - if (data > Threshold.LOW) { - color = colors.LOWER_RISK; - } - if (data > Threshold.MEDIUM) { - color = colors.MIDDLE_RISK; - } - if (data > Threshold.HIGH) { - color = colors.HIGHEST_RISK; - } - return color; - } - - render() { - const hasExposure = this.state.data.some(point => point.y > 0); - - return ( - - - - {languages.t('label.notification_title')} - - {this.state.dataAvailable ? ( - <> - - - t == 1 - ? languages.t('label.notification_today') - : t == 12 - ? languages.t('label.notification_2_weeks_ago') - : '' - } - /> - - datum.fill, - }, - }} - data={this.state.data} - /> - - - - - - {this.state.data.map(data => - data.y === 0 ? ( - data.x == max_exposure_window - 1 && !hasExposure ? ( - - {languages.t('label.notifications_no_exposure')} - - ) : ( - - ) - ) : ( - - - {data.x == 0 - ? languages.t( - 'label.notifications_exposure_format_today', - { exposureTime: data.y * bin_duration }, - ) - : data.x == 1 - ? languages.t( - 'label.notifications_exposure_format_yesterday', - { exposureTime: data.y * bin_duration }, - ) - : languages.t('label.notifications_exposure_format', { - daysAgo: data.x, - exposureTime: data.y * bin_duration, - })} - - - - ), - )} - What does this mean? - - Individuals who don't exhibit symptoms can sometimes still - carry the infection and infect others. Being careful about - social distancing and coming in contact with large groups or - at risk individuals (the elderly, those with significant other - medical issues) is important to manage both your risk and the - risk to others. - - - If you have no symptoms but still would like to be tested you - can go to your nearest testing site. - - - - ) : ( - <> - - {languages.t('label.notification_data_not_available')} - - - {languages.t('label.notification_warning_text')} - - this.goToSettings()}> - - {languages.t('label.notification_random_data_button')} - - - - )} - - - ); - } -} - -const styles = StyleSheet.create({ - // Container covers the entire screen - container: { - flex: 1, - flexDirection: 'column', - color: colors.PRIMARY_TEXT, - backgroundColor: colors.WHITE, - }, - main: { - flex: 1, - paddingVertical: 20, - width: '100%', - }, - row: { - flex: 1, - flexDirection: 'row', - color: colors.PRIMARY_TEXT, - backgroundColor: colors.WHITE, - }, - buttonTouchable: { - borderRadius: 12, - backgroundColor: '#665eff', - alignSelf: 'center', - width: width * 0.7866, - marginTop: 30, - justifyContent: 'center', - paddingVertical: 15, - width: '90%', - }, - buttonText: { - fontSize: 14, - lineHeight: 19, - fontFamily: fontFamily.primaryBold, - fontSize: 14, - lineHeight: 19, - letterSpacing: 0, - textAlign: 'center', - color: '#ffffff', - }, - mainText: { - fontSize: 18, - lineHeight: 24, - fontFamily: fontFamily.primaryRegular, - marginLeft: 20, - marginRight: 20, - marginBottom: 10, - marginTop: 20, - overflow: 'scroll', - }, - pageTitle: { - fontSize: 24, - fontFamily: fontFamily.primaryRegular, - marginLeft: 20, - }, - backArrow: { - height: 18, - width: 18.48, - }, - notificationsHeader: { - backgroundColor: '#665eff', - width: '100%', - paddingHorizontal: 20, - paddingVertical: 14, - }, - notificationsHeaderText: { - color: colors.WHITE, - fontSize: 18, - lineHeight: 22, - fontFamily: fontFamily.primaryBold, - }, - notificationView: { - width: '100%', - paddingVertical: 5, - paddingHorizontal: 20, - }, - notificationsText: { - color: colors.BLACK, - fontSize: 16, - paddingHorizontal: 10, - fontFamily: fontFamily.primaryRegular, - }, - noExposure: { - margin: 30, - color: 'forestgreen', - fontSize: 20, - textAlign: 'center', - fontFamily: fontFamily.primaryMedium, - }, - divider: { - marginTop: 15, - backgroundColor: Colors.DIVIDER, - height: 1.5, - }, - fullDivider: { - backgroundColor: Colors.DIVIDER, - height: 1, - width: '100%', - }, - whatNextHeader: { - fontSize: 24, - fontFamily: fontFamily.primaryBold, - paddingHorizontal: 20, - paddingTop: 20, - paddingBottom: 10, - }, - whatNextBody: { - fontSize: 20, - fontFamily: fontFamily.primaryRegular, - paddingHorizontal: 20, - paddingVertical: 5, - }, -}); - -export default NotificationScreen; diff --git a/app/views/Settings.js b/app/views/Settings.js index 9d58ea6107..b6cb004267 100644 --- a/app/views/Settings.js +++ b/app/views/Settings.js @@ -65,7 +65,7 @@ class SettingsScreen extends Component { } eventHistoryButtonPressed() { - this.props.navigation.navigate('NotificationScreen'); + this.props.navigation.navigate('ExposureHistoryScreen'); } licensesButtonPressed() { diff --git a/app/views/__tests__/Overlap.spec.js b/app/views/__tests__/Overlap.spec.js index 6ba9be2bfd..23bb4adc4c 100644 --- a/app/views/__tests__/Overlap.spec.js +++ b/app/views/__tests__/Overlap.spec.js @@ -1,9 +1,9 @@ import React from 'react'; -import {render} from '@testing-library/react-native'; +import { render } from '@testing-library/react-native'; import Overlap from '../Overlap'; it('renders correctly', () => { - const {asJSON} = render(); + const { asJSON } = render(); expect(asJSON()).toMatchSnapshot(); }); diff --git a/app/views/__tests__/__snapshots__/Import.spec.js.snap b/app/views/__tests__/__snapshots__/Import.spec.js.snap index 5c90430174..d176b4db76 100644 --- a/app/views/__tests__/__snapshots__/Import.spec.js.snap +++ b/app/views/__tests__/__snapshots__/Import.spec.js.snap @@ -7,7 +7,9 @@ Array [ style={ Object { "backgroundColor": "#4051DB", - "flex": 0, + "flexBasis": 0, + "flexGrow": 0, + "flexShrink": 1, } } />, @@ -16,7 +18,9 @@ Array [ style={ Object { "backgroundColor": "#F7F8FF", - "flex": 1, + "flexBasis": 0, + "flexGrow": 1, + "flexShrink": 1, } } > diff --git a/app/views/__tests__/__snapshots__/Licenses.spec.js.snap b/app/views/__tests__/__snapshots__/Licenses.spec.js.snap index db9a96b0d8..36cf7ab115 100644 --- a/app/views/__tests__/__snapshots__/Licenses.spec.js.snap +++ b/app/views/__tests__/__snapshots__/Licenses.spec.js.snap @@ -15,7 +15,9 @@ exports[`renders correctly 1`] = ` style={ Object { "backgroundColor": "#4051DB", - "flex": 0, + "flexBasis": 0, + "flexGrow": 0, + "flexShrink": 1, } } /> @@ -24,7 +26,9 @@ exports[`renders correctly 1`] = ` style={ Object { "backgroundColor": "#F7F8FF", - "flex": 1, + "flexBasis": 0, + "flexGrow": 1, + "flexShrink": 1, } } > diff --git a/app/views/__tests__/__snapshots__/News.spec.js.snap b/app/views/__tests__/__snapshots__/News.spec.js.snap index e88a2bb7bd..3cedc82aea 100644 --- a/app/views/__tests__/__snapshots__/News.spec.js.snap +++ b/app/views/__tests__/__snapshots__/News.spec.js.snap @@ -42,7 +42,9 @@ exports[`renders correctly 1`] = ` style={ Object { "backgroundColor": "#4051DB", - "flex": 0, + "flexBasis": 0, + "flexGrow": 0, + "flexShrink": 1, } } /> @@ -51,7 +53,9 @@ exports[`renders correctly 1`] = ` style={ Object { "backgroundColor": "#F7F8FF", - "flex": 1, + "flexBasis": 0, + "flexGrow": 1, + "flexShrink": 1, } } > diff --git a/app/views/__tests__/__snapshots__/Settings.spec.js.snap b/app/views/__tests__/__snapshots__/Settings.spec.js.snap index fa176bbeae..7c78f14610 100644 --- a/app/views/__tests__/__snapshots__/Settings.spec.js.snap +++ b/app/views/__tests__/__snapshots__/Settings.spec.js.snap @@ -15,7 +15,9 @@ exports[`renders correctly 1`] = ` style={ Object { "backgroundColor": "#4051DB", - "flex": 0, + "flexBasis": 0, + "flexGrow": 0, + "flexShrink": 1, } } /> @@ -24,7 +26,9 @@ exports[`renders correctly 1`] = ` style={ Object { "backgroundColor": "#F7F8FF", - "flex": 1, + "flexBasis": 0, + "flexGrow": 1, + "flexShrink": 1, } } > diff --git a/jestSetupFile.js b/jestSetupFile.js index aa2ac2be48..a89b0bb9f0 100644 --- a/jestSetupFile.js +++ b/jestSetupFile.js @@ -1,5 +1,13 @@ import mockAsyncStorage from '@react-native-community/async-storage/jest/async-storage-mock'; +import { NativeModules } from 'react-native'; + +// Device locale mocks +NativeModules.SettingsManager = NativeModules.SettingsManager || { + settings: { AppleLocale: 'en_US' }, + I18nManager: { localeIdentifier: 'en_US' }, +}; + // Silence YellowBox useNativeDriver warning jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); diff --git a/package.json b/package.json index 832fefe3b9..b111e5a98f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ ] }, "dependencies": { + "@emotion/core": "^10.0.28", + "@emotion/native": "^10.0.27", "@mauron85/react-native-background-geolocation": "^0.6.3", "@react-native-community/async-storage": "^1.8.1", "@react-native-community/geolocation": "^2.0.2", @@ -38,9 +40,10 @@ "@react-navigation/native": "5.0.9", "@react-navigation/stack": "5.1.1", "@testing-library/react-native": "^5.0.3", + "dayjs": "^1.8.24", + "emotion-theming": "^10.0.27", "i18next": "^19.3.3", "js-yaml": "^3.13.1", - "moment": "^2.24.0", "patch-package": "^6.2.1", "pluralize": "^8.0.0", "postinstall-postinstall": "^2.0.0", @@ -89,6 +92,7 @@ "lint-diff": "^1.2.1", "lint-staged": "^10.0.9", "metro-react-native-babel-preset": "^0.58.0", + "mockdate": "^2.0.5", "prettier": "1.19.1", "prettier-plugin-import-sort": "0.0.3", "react-native-svg-transformer": "^0.14.3", diff --git a/yarn.lock b/yarn.lock index f488944cdf..3f1772fada 100644 --- a/yarn.lock +++ b/yarn.lock @@ -153,7 +153,7 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-module-imports@^7.8.3": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== @@ -647,7 +647,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== @@ -731,6 +731,97 @@ dependencies: "@types/hammerjs" "^2.0.36" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + +"@emotion/core@^10.0.28": + version "10.0.28" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.28.tgz#bb65af7262a234593a9e952c041d0f1c9b9bef3d" + integrity sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@emotion/native@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/native/-/native-10.0.27.tgz#67c2c0ceeeed873c849c611d9a6497a006d43a8f" + integrity sha512-3qxR2XFizGfABKKbX9kAYc0PHhKuCEuyxshoq3TaMEbi9asWHdQVChg32ULpblm4XAf9oxaitAU7J9SfdwFxtw== + dependencies: + "@emotion/primitives-core" "10.0.27" + +"@emotion/primitives-core@10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/primitives-core/-/primitives-core-10.0.27.tgz#7a5fae07fe06a046ced597f5c0048f22d5c45842" + integrity sha512-fRBEDNPSFFOrBJ0OcheuElayrNTNdLF9DzMxtL0sFgsCFvvadlzwJHhJMSwEJuxwARm9GhVLr1p8G8JGkK98lQ== + dependencies: + css-to-react-native "^2.2.1" + +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -1844,6 +1935,22 @@ babel-plugin-dynamic-import-node@^2.3.0: dependencies: object.assign "^4.1.0" +babel-plugin-emotion@^10.0.27: + version "10.0.33" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03" + integrity sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + babel-plugin-istanbul@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" @@ -1862,6 +1969,20 @@ babel-plugin-jest-hoist@^25.2.1: dependencies: "@types/babel__traverse" "^7.0.6" +babel-plugin-macros@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= + babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: version "7.0.0-beta.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf" @@ -2137,6 +2258,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelize@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -2626,6 +2752,11 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= + css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" @@ -2651,6 +2782,15 @@ css-select@~1.2.0: domutils "1.5.1" nth-check "~1.0.1" +css-to-react-native@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.3.2.tgz#e75e2f8f7aa385b4c3611c52b074b70a002f2e7d" + integrity sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^3.3.0" + css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -2701,6 +2841,11 @@ cssstyle@^2.0.0: dependencies: cssom "~0.3.6" +csstype@^2.5.7: + version "2.6.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" + integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -2813,6 +2958,11 @@ dayjs@^1.8.15: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.23.tgz#07b5a8e759c4d75ae07bdd0ad6977f851c01e510" integrity sha512-NmYHMFONftoZbeOhVz6jfiXI4zSiPN6NoVWJgC0aZQfYVwzy/ZpESPHuCcI0B8BUMpSJQ08zenHDbofOLKq8hQ== +dayjs@^1.8.24: + version "1.8.24" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.24.tgz#2ef8a2ab9425eaf3318fe78825be1c571027488d" + integrity sha512-bImQZbBv86zcOWOq6fLg7r4aqMx8fScdmykA7cSh+gH1Yh8AM0Dbw0gHYrsOrza6oBBnkK+/OaR+UAa9UsMrDw== + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" @@ -3125,6 +3275,15 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emotion-theming@^10.0.27: + version "10.0.27" + resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.0.27.tgz#1887baaec15199862c89b1b984b79806f2b9ab10" + integrity sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/weak-memoize" "0.2.5" + hoist-non-react-statics "^3.3.0" + enabled@1.0.x: version "1.0.2" resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93" @@ -3818,7 +3977,7 @@ find-line-column@^0.5.2: resolved "https://registry.yarnpkg.com/find-line-column/-/find-line-column-0.5.2.tgz#db00238ff868551a182e74a103416d295a98c8ca" integrity sha1-2wAjj/hoVRoYLnShA0FtKVqYyMo= -find-root@^1.0.0: +find-root@^1.0.0, find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== @@ -4262,6 +4421,13 @@ hoist-non-react-statics@^2.3.1: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -6377,10 +6543,10 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws= -moment@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +mockdate@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.5.tgz#70c6abf9ed4b2dae65c81dfc170dd1a5cec53620" + integrity sha512-ST0PnThzWKcgSLyc+ugLVql45PvESt3Ul/wrdV/OPc/6Pr8dbLAIJsN1cIp41FLzbN+srVTNIRn+5Cju0nyV6A== morgan@^1.9.0: version "1.10.0" @@ -7119,6 +7285,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postinstall-postinstall@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" @@ -7336,7 +7507,7 @@ react-is@^16.12.0, react-is@^16.8.4, react-is@^16.9.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== -react-is@^16.13.0, react-is@^16.8.1: +react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -8303,7 +8474,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= -source-map@^0.5.0, source-map@^0.5.6: +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= From 4f43ac7484c16ab739522acd31fd67cee335f42d Mon Sep 17 00:00:00 2001 From: Steve Penrod Date: Tue, 14 Apr 2020 11:31:51 -0500 Subject: [PATCH 12/12] Fix usage of URL from a Healthcare Authority (#507) * Fix usage of URL from a Healthcare Authority The webview was using the wrong name for the webview ("url" instead of "news_url"). * Update yarn tests --- app/views/News.js | 6 +++--- app/views/__tests__/__snapshots__/News.spec.js.snap | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/News.js b/app/views/News.js index 9a3a21d822..2e1299c734 100644 --- a/app/views/News.js +++ b/app/views/News.js @@ -15,9 +15,9 @@ import languages from './../locales/languages'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; import colors from '../constants/colors'; import Colors from '../constants/colors'; -import { AUTHORITY_NEWS } from '../constants/storage'; // import { Colors } from 'react-native/Libraries/NewAppScreen'; import fontFamily from '../constants/fonts'; +import { AUTHORITY_NEWS } from '../constants/storage'; import { GetStoreData } from '../helpers/General'; const width = Dimensions.get('window').width; @@ -28,7 +28,7 @@ class NewsScreen extends Component { super(props); let default_news = { name: languages.t('label.default_news_site_name'), - url: languages.t('label.default_news_site_url'), + news_url: languages.t('label.default_news_site_url'), }; this.state = { visible: true, @@ -62,7 +62,7 @@ class NewsScreen extends Component {