Compare commits

...

174 Commits

Author SHA1 Message Date
bl00dymarie 4fab8b26cb Setting for ITSAppUsesNonExemptEncryption 2022-08-11 08:29:26 +02:00
bl00dymarie deb9d3d8e8 Bump version number 1.2208.11 2022-08-11 08:20:25 +02:00
bl00dymarie fcc54566fc Merge branch 'fix/replace-app-restart' into 'release'
Replaces closing/restart functionality with a more friendly app state reset

See merge request bloodyhealth/drip!461
2022-08-11 06:16:17 +00:00
Sofiya Tepikin 0118fcd6ce Merge branch 'Chore/Remove-email-and-hence-typo' into 'release'
Chore: Remove email and hence typo from text

See merge request bloodyhealth/drip!459
2022-08-10 14:35:36 +00:00
Sofiya Tepikin 298eeafdba Fixes linter failing 2022-08-10 16:24:37 +02:00
Sofiya Tepikin 77ea075c23 Cleanup 2022-08-10 16:24:37 +02:00
Sofiya Tepikin 38f91c2e25 Reset app state without closing/restarting it 2022-08-10 16:24:37 +02:00
Sofiya Tepikin d9a1cd7895 Prettify files before other changes
Prettify
2022-08-10 16:24:37 +02:00
Sofiya Tepikin e954ddf991 Merge branch 'fix/realm-warning' into 'release'
Fix deprecated way of using writeCopyTo - realm method

See merge request bloodyhealth/drip!460
2022-08-10 14:22:56 +00:00
Sofiya Tepikin 74c570da38 Fix deprecated way of using writeCopyTo - realm method 2022-08-10 14:22:55 +00:00
bl00dymarie 8c99f2c6a3 Remove email and hence typo from text 2022-08-09 13:16:58 +02:00
bl00dymarie 34c1eae991 Merge branch 'fix/empty-password' into 'release'
Fix/empty password

See merge request bloodyhealth/drip!456
2022-08-08 11:33:56 +00:00
bl00dymarie e37d44c506 Bump version number 1.2208.8 2022-08-08 13:31:17 +02:00
Sofiya Tepikin 33dba03c47 Disable button when no input 2022-08-08 12:38:20 +02:00
Sofiya Tepikin a7bdd4b6a6 Prettify enter-new-password 2022-08-08 12:37:20 +02:00
bl00dymarie 1c1d64e719 Bump version to 1.2208.2 2022-08-02 23:07:36 +02:00
Sofiya Tepikin 95ce1b6768 Force light mode for iOS 2022-08-01 12:46:09 +02:00
Sofiya Tepikin f73564a1a4 Merge branch 'Chore/Bump-version-of-the-day-and-potentially-beta-test' into 'master'
Chore/bump version of the day and potentially beta test

See merge request bloodyhealth/drip!413
2022-07-10 17:41:16 +00:00
bl00dymarie 5c235b5197 Update https website link 2022-07-10 19:34:01 +02:00
bl00dymarie 31bcece857 Bump version number 1.2207.10 2022-07-10 19:33:11 +02:00
bl00dymarie aa12f8f249 Merge branch 'fix/donate-button-fork' into 'master'
Cleanup donation button fork on iOS & update the link to the website

Closes #572 and #566

See merge request bloodyhealth/drip!412
2022-07-10 17:27:59 +00:00
Sofiya Tepikin 800b831958 Update the link to our website with the brand new domain 2022-07-10 19:22:30 +02:00
Sofiya Tepikin 3b45a32727 Cleanup donation button fork on iOS 2022-07-10 19:04:30 +02:00
bl00dymarie c311900e27 Merge branch '546-bug-cycle-day-title-covers-navigation-on-upper-right-2' into 'master'
Fix: Make date format more compact for cycle day

Closes #546

See merge request bloodyhealth/drip!411
2022-07-10 16:46:04 +00:00
bl00dymarie 71d81904ad Make date format more compact for cycle day 2022-07-10 18:11:02 +02:00
BloodyMarie 6fb98f7193 Update text for support 2022-07-06 23:08:12 +02:00
BloodyMarie 8721bc484c Feature: Remove donate button for iOS 2022-07-06 23:06:46 +02:00
Sofiya Tepikin 68d8a55034 Merge branch 'fix/weblate-parse-error' into 'master'
Fix weblate parse error

See merge request bloodyhealth/drip!408
2022-07-05 18:13:11 +00:00
Sofiya Tepikin 36a77111ce Fix invalid json file 2022-07-05 16:52:39 +02:00
Sofiya Tepikin 1e504a6143 Merge branch 'feature/Add-privacy-policy' into 'master'
Feature: Adds privacy policy

See merge request bloodyhealth/drip!399
2022-07-03 20:28:39 +00:00
Lisa 2b116c375d Merge branch 'chore/update-husky' into 'master'
Update husky version 6 -> 8

See merge request bloodyhealth/drip!405
2022-07-03 18:08:22 +00:00
Sofiya Tepikin c35afa2dbf Merge branch 'chalkedgoose/rm-flowconfig' into 'master'
Remove .flowconfig

See merge request bloodyhealth/drip!407
2022-07-02 08:51:14 +00:00
Sofiya Tepikin 6b441294d9 Update husky version 7 -> 8 2022-06-30 13:38:04 +02:00
Carlos Alba 99069bac8e Remove .flowconfig 2022-06-26 18:10:40 -07:00
Lisa 3633108006 Merge branch 'chore/add-rn-exit-app-library' into 'master'
Chore: add rn-exit-app-v2 library

See merge request bloodyhealth/drip!401
2022-06-25 10:19:01 +00:00
Sofiya Tepikin 7a6643cc1a Add alert before closing 2022-06-23 21:46:41 +02:00
Sofiya Tepikin f223616ee1 Fork restart functionality for different platforms 2022-06-23 21:40:15 +02:00
BloodyMarie ae2a05f9b0 Add package-lock.json after clear script 2022-06-23 17:42:05 +02:00
Maria Zadnepryanets 81cc3087fd Update wordings to advise user that app will close 2022-06-23 17:42:05 +02:00
Maria Zadnepryanets d8c4278fec Add react-native-exit-app-v2 2022-06-23 17:42:05 +02:00
Sofiya Tepikin e87d569b8b Update husky version 6 -> 7 2022-06-23 17:09:55 +02:00
bl00dymarie 829b4bf242 Merge branch 'chore/Bump-version-number' into 'master'
Chore/bump version number to 1.2206.19

See merge request bloodyhealth/drip!402
2022-06-22 09:58:13 +00:00
BloodyMarie dcf7c07a85 Specify node and npm version 2022-06-21 09:42:16 +02:00
BloodyMarie 4f24357420 Add missing 'key' prop for element in iterator 2022-06-20 16:57:20 +02:00
BloodyMarie 3b187a5a4e Feature: Adds privacy policy 2022-06-19 23:26:06 +02:00
BloodyMarie 5b9d904a02 Update package-lock after rebase 2022-06-19 23:08:36 +02:00
BloodyMarie f018332dcc Prepare for archiving 2022-06-19 23:01:27 +02:00
BloodyMarie 1e94c6d7c9 Bump version number to 1.2206.19 2022-06-19 23:01:24 +02:00
Sofiya Tepikin 40f9ea23f3 Merge branch 'fix/ci-pipeline' into 'master'
Regenerate package-lock.json with correct npm version

See merge request bloodyhealth/drip!403
2022-06-19 18:35:24 +00:00
Sofiya Tepikin 33a657cda4 Regenerate package-lock.json with correct npm version 2022-06-19 18:35:24 +00:00
Sofiya Tepikin 229f864b54 Update .gitlab-ci.yml file 2022-06-19 17:52:02 +00:00
bl00dymarie 09c808f99b Merge branch 'chore/realm-upgrade' into 'master'
Chore/realm upgrade

See merge request bloodyhealth/drip!393
2022-06-19 17:04:30 +00:00
Maria Zadnepryanets 37b607aee2 Bump up the node version of the image in the CI pipline 2022-06-19 19:01:50 +02:00
bl00dymarie f56b94463f Merge branch 'Fix/rename-drip-for-ios' into 'master'
Rename drip to drip.

See merge request bloodyhealth/drip!400
2022-06-19 15:51:34 +00:00
bl00dymarie c2af69cfec Revert README changes 2022-06-19 15:04:05 +00:00
BloodyMarie 8b3a5359e8 Rename to drip. 2022-06-19 16:57:11 +02:00
BloodyMarie 670857f5ff Rename drip to drip. 2022-06-19 16:41:52 +02:00
bl00dymarie 52094573e2 Merge branch 'Fix/Switch-options-in-import-text' into 'master'
Fix: Switch button order to match explainer text

See merge request bloodyhealth/drip!397
2022-06-19 13:34:47 +00:00
bl00dymarie 1f3cdb9438 Merge branch 'chore/start-on-xcode13' into 'chore/realm-upgrade'
Chore/start on xcode13

See merge request bloodyhealth/drip!394
2022-05-26 13:51:33 +00:00
BloodyMarie 6b5ed65e90 Switch button order to match explainer text 2022-05-26 14:22:55 +02:00
Sofiya Tepikin 9940c2c46e Map realm object on the export only 2022-05-01 16:33:57 +02:00
bl00dymarie f7fc81d865 Merge branch '542-bug-styling' into 'master'
Resolve "Bug: Styling"

Closes #542

See merge request bloodyhealth/drip!391
2022-04-30 16:37:49 +00:00
mashazyu 10b9ec8818 Pachage-lock.json update after rebase 2022-04-25 22:05:59 +02:00
mashazyu 712f65e001 Make app buils on Xcode13 2022-04-25 21:59:25 +02:00
Sofiya Tepikin 7d109878fc Fix sex, mood & pain symptoms not showing data on the overview 2022-04-24 22:04:10 +02:00
Sofiya Tepikin bce21ff9a9 Fix when the object from db is empty 2022-04-24 21:48:36 +02:00
Sofiya Tepikin 95b0cc5059 Update readme 2022-04-23 22:17:12 +02:00
Sofiya Tepikin f0e6cae055 Update realm version 3.6.5 -> 10.16.0 2022-04-23 22:16:32 +02:00
BloodyMarie dea67c88f5 Chart styling adaption: Temporarily disbale temp scale settings 2022-04-19 23:20:43 +02:00
BloodyMarie 8a29f08dca Cycle Day styling improvements 2022-04-19 22:40:41 +02:00
BloodyMarie 655f6b31d8 Cycle day styling: Remove new line for better display of mucus 2022-04-19 22:40:41 +02:00
BloodyMarie 0cebe910c4 Remove idle code 2022-04-19 22:40:41 +02:00
BloodyMarie 77c7a57463 Stats styling improvement 2022-04-18 19:50:36 +02:00
bl00dymarie 342f9798b2 Merge branch 'Fix-title-for-picker' into 'master'
Fix: Specify the title for timePicker in IOS

See merge request bloodyhealth/drip!387
2022-04-15 21:22:03 +00:00
bl00dymarie 9cfc93cd8e Merge branch '539-bug-values-for-sex-mood-pain-symptom-boxes-on-overviews-are-not-shown' into 'master'
Revert "Update realm version 3.6.5 -> 5.0.9"

Closes #539

See merge request bloodyhealth/drip!386
2022-04-15 19:45:13 +00:00
BloodyMarie 977ed07d97 Add picker title to labels 2022-04-15 21:40:14 +02:00
BloodyMarie 1813bf82f9 Fix: Specify the title for timePicker in IOS 2022-04-15 21:29:36 +02:00
bl00dymarie 9b5a717c7d Merge branch '540-Temperature-value-cannot-be-entered-with-comma' into 'master'
Resolve "Bug: Temperature value cannot be entered with comma"

Closes #540

See merge request bloodyhealth/drip!390
2022-04-15 19:23:04 +00:00
bl00dymarie 76f81841f4 Merge branch '537-bug-temperature-reminder-on-ios' into 'master'
Fix: Target reminder with correct id

Closes #537

See merge request bloodyhealth/drip!389
2022-04-15 17:29:19 +00:00
Lisa Hillebrand 585017eadd 540 Refactor temperature to functional component 2022-04-15 19:03:18 +02:00
Lisa Hillebrand fe9d9b4fdc 540 Rename method to get previous temperature 2022-04-15 17:41:53 +02:00
bl00dymarie a37607eae6 fix indentation 2022-04-15 14:06:06 +00:00
bl00dymarie 54bc836811 fix indentation 2022-04-15 14:05:41 +00:00
BloodyMarie 992161e3ce Implement review feedback 2022-04-15 16:03:17 +02:00
Lisa 534f554986 Merge branch 'chore/Swap-icon-for-learn-more' into 'master'
Chore: exchange icons for learn more

See merge request bloodyhealth/drip!388
2022-04-15 13:47:33 +00:00
BloodyMarie 407fa834ab Make temp reminder work for android & ios 2022-04-11 22:22:55 +02:00
BloodyMarie 16d2afaf1e Fix: Target reminder with correct id 2022-04-10 23:04:09 +02:00
BloodyMarie 765a8ae3b2 Chore: exchange icons for learn more 2022-04-10 21:23:12 +02:00
BloodyMarie 0eb3c93a17 Revert "Update readme and engines definition in package.json"
This reverts commit cce8e4ef0e.
2022-03-28 00:35:10 +02:00
BloodyMarie 96d7deb47c Revert "Update realm version 3.6.5 -> 5.0.9"
This reverts commit 7d6a577eeb.
2022-03-28 00:34:33 +02:00
bl00dymarie 2a49e43065 Merge branch 'chore/update-styling' into 'master'
Chore/update styling

See merge request bloodyhealth/drip!374
2022-03-26 12:15:59 +00:00
bl00dymarie 6aef594b29 Adding paddingTop to title of symptom box 2022-03-26 13:13:27 +01:00
MariaZ eb53b8b87e Return some button styling to fix button display on simptom edit 2022-03-26 13:13:27 +01:00
MariaZ f34df0233c Increase box size and change back number of lines to display to 4 2022-03-26 13:13:27 +01:00
MariaZ 37e1d54358 More fine-tuning of calendar and symptom box 2022-03-26 13:13:27 +01:00
MariaZ 7faa18bd60 Remove fontratio (I think we don't need it any more) 2022-03-26 13:13:27 +01:00
MariaZ 3c02dd77bb Update buttons styling to make them not that horizontally big 2022-03-26 13:13:27 +01:00
MariaZ 272b1f387d Font size fine-tuning 2022-03-26 13:13:27 +01:00
MariaZ c5aaf1b29b Add note to remove images fix after RN update 2022-03-26 13:13:27 +01:00
MariaZ 2bbcadcf53 Elipsise date on day overview 2022-03-26 13:13:27 +01:00
MariaZ f4ef00d4ea Fix adding images to final build 2022-03-26 13:13:27 +01:00
MariaZ 953e080032 Fix symptom-box text overflow 2022-03-26 13:13:27 +01:00
MariaZ 10fdcecf61 Fix buttons on simptom edit view for small screens 2022-03-26 13:13:27 +01:00
MariaZ cfef925414 Fix styling 2022-03-26 13:13:27 +01:00
MariaZ 7cab47665f Fix today day styling in calendar 2022-03-26 13:13:27 +01:00
MariaZ 9fb08fb66f Fix linter errors 2022-03-26 13:13:27 +01:00
MariaZ 388985034f Update symptom box styling 2022-03-26 13:13:27 +01:00
MariaZ f842ebe13c Update stats page styling 2022-03-26 13:13:27 +01:00
MariaZ 070c1487af Install react-native-size-matters library for better layout 2022-03-26 13:13:27 +01:00
bl00dymarie a624b5c015 Merge branch '535-chore-remove-unsupported-platforms-for-gradle' into 'master'
Chore: Remove unspported platforms for gradle

Closes #535

See merge request bloodyhealth/drip!383
2022-03-26 10:54:00 +00:00
Lisa 7f945c9fdd Merge branch '432-button-for-email-contact-instead-of-hyperlink' into 'master'
Chore: Adds buttons instead of links to about

Closes #432

See merge request bloodyhealth/drip!384
2022-03-26 10:52:09 +00:00
BloodyMarie d6a18bb44d Remove unused file 2022-03-26 11:46:32 +01:00
bl00dymarie 09de52b5df Dot notation 2022-03-26 11:46:32 +01:00
bl00dymarie 1584d2d368 Dot notation 2022-03-26 11:46:32 +01:00
BloodyMarie d1ac12d165 Chore: Implement review feedback 2022-03-26 11:46:32 +01:00
bl00dymarie 03b359019e Chore: Adds buttons instead of links to about 2022-03-26 11:46:29 +01:00
Lisa b93983243e Merge branch '511-use-translation-library-for-license-component' into 'master'
Use translation library for license component

Closes #511

See merge request bloodyhealth/drip!373
2022-03-26 10:21:26 +00:00
bl00dymarie 009b6b38e1 Correct email 2022-03-26 10:19:15 +00:00
bl00dymarie f462b349fc Merge branch 'chore/update-realm' into 'master'
Update realm version 3.6.5 -> 5.0.9

See merge request bloodyhealth/drip!380
2022-03-25 21:48:36 +00:00
bl00dymarie 64124c2fd5 Chore: Remove unspported platforms for gradle 2022-02-06 14:13:40 +01:00
Sofiya Tepikin cce8e4ef0e Update readme and engines definition in package.json 2022-01-23 17:41:50 +01:00
Sofiya Tepikin 7d6a577eeb Update realm version 3.6.5 -> 5.0.9 2022-01-23 17:40:26 +01:00
Lisa dc8f829d34 Merge branch '518-replace-deprecated-babel-parser-for-eslint' into 'master'
518 Replace deprecated eslint parser

Closes #518

See merge request bloodyhealth/drip!378
2021-12-01 10:50:18 +00:00
Lisa Hillebrand 1f5ba4de12 518 Run eslint on missing directories 2021-11-28 16:16:35 +01:00
Lisa Hillebrand fbc561622c 518 Update eslint config 2021-11-28 16:11:15 +01:00
Lisa Hillebrand 426d35ab78 518 Replace deprecated eslint parser 2021-11-28 16:10:48 +01:00
Maria Zadnepryanets 1ec6fd9296 Merge branch 'master' into 'master'
Update README.md to linkt to Version 8 instread of Version 5

See merge request bloodyhealth/drip!379
2021-11-28 13:10:14 +00:00
bkqtnte10 a53c8ce4f7 Update README.md to linkt to Version 8 instread of Version 5 2021-10-20 05:33:54 +00:00
Sofiya Tepikin c6ffdaa46e Merge branch '517-Install-husky-and-prettier-to-format-staged-files' into 'master'
Install husky and prettier to format staged files

Closes #517

See merge request bloodyhealth/drip!377
2021-09-19 14:11:59 +00:00
Lisa 3625d5a4bb Install husky and prettier to format staged files 2021-09-19 14:11:59 +00:00
Maria Zadnepryanets 50d01cbaa4 Merge branch 'chore/add-dependabot-to-repo' into 'master'
Adds dependabot to repo

See merge request bloodyhealth/drip!375
2021-09-19 13:18:46 +00:00
bl00dymarie 1e734082af Merge branch 'chore/update-readme' into 'master'
Add info on nvm requirement and xcode 12.5 fix

See merge request bloodyhealth/drip!372
2021-09-19 12:46:26 +00:00
Maria Zadnepryanets 449c84e75e Add info on nvm requirement and xcode 12.5 fix 2021-09-19 12:46:26 +00:00
bl00dymarie f282e24308 Merge branch 'feature/add-toast-on-ios' into 'master'
Feature: add toast on ios

Closes #321

See merge request bloodyhealth/drip!370
2021-09-19 10:07:58 +00:00
bl00dymarie d1efd1d587 Merge branch '510-replace-metadata-folder-with-redesigned' into 'master'
Resolve "Replace metadata folder with redesigned"

Closes #510

See merge request bloodyhealth/drip!368
2021-09-05 12:17:24 +00:00
MariaZ 4d32c523ff Adds dependabot config 2021-08-29 20:18:51 +02:00
MariaZ d2a452e3c9 Add toast to saving/deleting symptom data 2021-08-29 14:08:37 +02:00
MariaZ 4bff5a3d68 Move toast showing function to helper 2021-08-29 14:03:29 +02:00
MariaZ 233d14968d Package-lock.json commit 2021-08-20 20:10:00 +02:00
Lisa Hillebrand 9596f8e52f 511 Use translation function in license components 2021-08-15 17:37:22 +02:00
Lisa Hillebrand 4f93d30872 511 Rename component 2021-08-15 16:57:04 +02:00
MariaZ f188f018b9 Refactor existing Toast 2021-08-15 16:15:10 +02:00
MariaZ 65d0e4f3a1 Install react-native-simple-toast 2021-08-15 16:07:04 +02:00
emelko c60347badf Update phone screenshots 2021-08-15 14:05:30 +02:00
emelko 6c1fa662f9 Update description text 2021-08-15 14:05:30 +02:00
emelko 0bf7f2525e Update icon and banner 2021-08-15 14:05:30 +02:00
Maria Zadnepryanets 7c70f7454e Merge branch 'chore/add-react-native-clean-project' into 'master'
Chore: add react native clean project

Closes #431

See merge request bloodyhealth/drip!367
2021-08-15 11:56:09 +00:00
Maria Zadnepryanets 4fedb1928c Chore: add react native clean project 2021-08-15 11:56:09 +00:00
Maria Zadnepryanets 87a68ba9c5 Merge branch 'chore/add-instructions-for-ios' into 'master'
Chore: adds instructions on how to run app on ios to readme

Closes #333

See merge request bloodyhealth/drip!358
2021-08-06 15:02:18 +00:00
Maria Zadnepryanets 4b06f03aec Chore: adds instructions on how to run app on ios to readme 2021-08-06 15:02:15 +00:00
Maria Zadnepryanets 948c7c0b24 Merge branch 'ios-launch-screen-and-icons' into 'master'
iOS assets: icons & launch screen

See merge request bloodyhealth/drip!365
2021-08-06 14:58:57 +00:00
sdvig 40083d819f Add ios icons and launch screen 2021-07-30 16:45:57 +02:00
bl00dymarie c5162beb3b Update Twitter handle 2021-07-30 12:12:33 +00:00
Lisa f65d06edb3 Update README.md 2021-06-18 12:07:17 +00:00
bl00dymarie e08c6be97c Merge branch 'weblate-drip-test-component' into 'master'
Translations update from Weblate

See merge request bloodyhealth/drip!362
2021-06-18 11:44:51 +00:00
bl00dymarie ea669c1fac Added translation using Weblate (German) 2021-06-18 13:33:17 +02:00
bl00dymarie 940c7806ee Merge branch '162-Prepare-localization' into 'master'
Resolve "Prepare localization"

Closes #162

See merge request bloodyhealth/drip!361
2021-06-18 11:11:28 +00:00
Lisa Hillebrand 8b8ae0d436 162 Update structure of english translation file 2021-06-18 11:37:00 +02:00
Lisa Hillebrand 3fd9cc0e02 162 Fix lint issue 2021-05-02 22:17:41 +02:00
Lisa Hillebrand 1514e21726 162 Remove component path from translation key 2021-05-02 22:02:07 +02:00
Lisa Hillebrand 0b447178c5 162 Use hook to merge component path with translation key 2021-05-02 21:45:46 +02:00
Lisa Hillebrand f3cabe5ca1 162 Use translation function in home component 2021-05-02 21:35:30 +02:00
Lisa Hillebrand e0f64173bf 162 Refactor Home component to functional component 2021-05-02 21:19:45 +02:00
Lisa Hillebrand 36ce29c346 162 Uppercase home component name 2021-05-02 21:09:21 +02:00
Lisa Hillebrand 4676c50504 162 Add configuration for i18next 2021-05-02 20:51:15 +02:00
Lisa Hillebrand 886a952e53 162 Install dependencies for i18next 2021-05-02 20:48:38 +02:00
Sofiya Tepikin 5b1544c8f4 Merge branch 'feature/ios-launch' into 'master'
Feature/ios launch

See merge request bloodyhealth/drip!351
2021-05-02 16:31:18 +00:00
Sofiya Tepikin f0155b342f Feature/ios launch 2021-05-02 16:31:17 +00:00
Maria Zadnepryanets e532c3d94c Merge branch 'upgrade-rn-and-friends' into 'master'
Upgrade RN and friends

Closes #491

See merge request bloodyhealth/drip!336
2021-04-18 12:55:22 +00:00
Sofiya Tepikin 2535d056b7 Upgrade RN and friends 2021-04-18 12:55:21 +00:00
Maria Zadnepryanets 9ff117ce4d Merge branch '402-give-export-file-more-specific-name' into 'master'
Give exported csv drip-data name

Closes #402

See merge request bloodyhealth/drip!359
2021-04-11 13:22:00 +00:00
emelko 78e4d109c7 Give exported csv drip-data name 2021-03-17 22:12:24 +01:00
bl00dymarie aa2de9e335 Update CONTRIBUTING.md 2021-03-13 17:30:02 +00:00
bl00dymarie e78337a8b3 Update CONTRIBUTING.md 2021-03-13 17:17:48 +00:00
153 changed files with 6203 additions and 9853 deletions
-42
View File
@@ -1,42 +0,0 @@
{
"env": {
"node": true,
"mocha": true,
"es6": true
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018
},
"plugins": ["react"],
"settings": {
"react": {
"version": require("./package.json").dependencies.react
}
},
"rules": {
"indent": ["error", 2],
"no-console": ["error", { "allow": ["warn", "error"] }],
"space-before-function-paren": 0,
"semi": ["warn", "never"],
"space-infix-ops": ["warn"],
"no-var": "error",
"prefer-const": "error",
"no-trailing-spaces": "error",
"react/prop-types": 2,
"max-len": [
1,
{
"ignoreStrings": true,
"ignoreComments": true,
"ignoreTemplateLiterals": true
}
],
"no-multi-spaces": 2
}
}
+33
View File
@@ -0,0 +1,33 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'plugin:react/recommended'],
env: {
node: true,
mocha: true,
es6: true,
},
parser: '@babel/eslint-parser',
parserOptions: {
requireConfigFile: false,
babelOptions: {
presets: ['@babel/preset-react'],
},
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
},
plugins: ['react'],
settings: {
react: {
version: require('./package.json').dependencies.react,
},
},
rules: {
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-var': 'error',
'prefer-const': 'error',
'react/prop-types': 2,
},
}
+5 -1
View File
@@ -23,7 +23,7 @@ DerivedData
*.hmap *.hmap
*.ipa *.ipa
*.xcuserstate *.xcuserstate
project.xcworkspace xcshareddata
ios/Index/DataStore ios/Index/DataStore
# Android/IntelliJ # Android/IntelliJ
@@ -44,6 +44,7 @@ yarn-error.log
buck-out/ buck-out/
\.buckd/ \.buckd/
*.keystore *.keystore
!debug.keystore
# fastlane # fastlane
# #
@@ -59,6 +60,9 @@ buck-out/
# Bundle artifact # Bundle artifact
*.jsbundle *.jsbundle
# CocoaPods
/ios/Pods/
# RN android release # RN android release
android/app/bin/ android/app/bin/
android/app/release/ android/app/release/
+4 -4
View File
@@ -1,12 +1,12 @@
image: node:8 image: node:14
# This folder is cached between builds # This folder is cached between builds
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache # http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache: cache:
paths: paths:
- node_modules/ - node_modules/
test_async: test_async:
script: script:
- npm install - npm install
- npm test - npm test
+16
View File
@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 3
reviewers:
- "bl00dymarie"
- "mariyaz"
- "sdvig"
- "LisaHill"
allow:
- dependency-type: direct
- dependency-type: production
rebase-strategy: "auto"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx pretty-quick --staged
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1
View File
@@ -0,0 +1 @@
v14.19.3
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
singleQuote: true,
semi: false,
}
+1 -1
View File
@@ -14,7 +14,7 @@ So good to see you here, hello :wave\_tone1: :wave\_tone2: :wave\_tone3: :wave\_
## TL;DR ## TL;DR
You just want to say hello? Send us a [nice email](mailto:drip@mailbox.org?Subject=Nice%20incoming%20mail) :postbox: or tweet :bird: at us [@bl00dyhealth](https://twitter.com/bl00dyhealth). You just want to say hello? Send us a [nice email](mailto:drip@mailbox.org?Subject=Nice%20incoming%20mail) :postbox:, ask to [join our Slack](mailto:drip@mailbox.org?Subject=Join%20Slack) or tweet :bird: at us [@dripberlin](https://twitter.com/dripberlin).
## What should I know before I get started? ## What should I know before I get started?
+105 -58
View File
@@ -1,11 +1,11 @@
# drip, the open-source cycle tracking app # drip, the open-source cycle tracking app
A menstrual cycle tracking app that's open-source and leaves your data on your phone. Use it to track your menstrual cycle and/or for fertility awareness! A menstrual cycle tracking app that's open-source and leaves your data on your phone. Use it to track your menstrual cycle and/or for fertility awareness!
Find more information on [our website](https://bloodyhealth.gitlab.io/). Find more information on [our website](https://dripapp.org/).
[<img src="https://bloodyhealth.gitlab.io/assets/get.png" [<img src="https://dripapp.org/assets/get.png"
alt="Get it here" alt="Get it here"
height="55">](https://bloodyhealth.gitlab.io/release/5.apk) height="55">](https://dripapp.org/release/8.apk)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.drip/) height="80">](https://f-droid.org/packages/com.drip/)
@@ -15,37 +15,35 @@ Find more information on [our website](https://bloodyhealth.gitlab.io/).
The app is built in React Native and currently developed for Android. The app is built in React Native and currently developed for Android.
▶ [How to contribute to the project](https://gitlab.com/bloodyhealth/drip/blob/master/CONTRIBUTING.md) ▶ [How to contribute to the project](https://gitlab.com/bloodyhealth/drip/blob/master/CONTRIBUTING.md)
▶ [How to release a new version](https://gitlab.com/bloodyhealth/drip/blob/master/RELEASE.md) ▶ [How to release a new version](https://gitlab.com/bloodyhealth/drip/blob/master/RELEASE.md)
## Development setup ## Development setup
#### 1. Android Studio #### 1. Android Studio
Install [Android Studio](https://developer.android.com/studio/) - you'll need it to install some dependencies. Install [Android Studio](https://developer.android.com/studio/) - you'll need it to install some dependencies.
#### 2. Node version #### 2. Node & npm version
Make sure you are running Node 10 (newer versions won't work). It's easiest to switch Node versions using `nvm`, here's how to do it:
Make sure you are running Node 14 and npm 6.14.17. It's easiest to switch Node versions using `nvm`, here's how to do it:
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash $ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
$ nvm install v10 $ nvm install v14.19.3
#### 3. Get this repository #### 3. Get this repository
Clone it with SSH Clone it with SSH
$ git clone git@gitlab.com:bloodyhealth/drip.git $ git clone git@gitlab.com:bloodyhealth/drip.git
or clone it with HTTPS or clone it with HTTPS
$ git clone https://gitlab.com/bloodyhealth/drip.git $ git clone https://gitlab.com/bloodyhealth/drip.git
and run and run
$ cd drip $ cd drip
$ npm install $ npm install
@@ -53,94 +51,139 @@ and run
Open Android Studio and click on "Open an existing Android Studio project". Navigate to the drip repository you cloned and double click the android folder. It detects, downloads and cofigures requirements that might be missing, like the NDK and CMake to build the native code part of the project. Also see the [nodejs-mobile repository](https://github.com/janeasystems/nodejs-mobile) for the necessary prerequisites for your system. Open Android Studio and click on "Open an existing Android Studio project". Navigate to the drip repository you cloned and double click the android folder. It detects, downloads and cofigures requirements that might be missing, like the NDK and CMake to build the native code part of the project. Also see the [nodejs-mobile repository](https://github.com/janeasystems/nodejs-mobile) for the necessary prerequisites for your system.
#### 5. Run the app #### 5. Run the app on Android
Either start a [virtual device in Android Studio](https://developer.android.com/studio/run/emulator) or [set your physical device like your Android phone up](https://developer.android.com/training/basics/firstapp/running-app) to run the app. Either start a [virtual device in Android Studio](https://developer.android.com/studio/run/emulator) or [set your physical device like your Android phone up](https://developer.android.com/training/basics/firstapp/running-app) to run the app.
1. Open a terminal and run 1. Open a terminal and run
```
$ npm run android
```
1. To see logging output, run the following command in another tab: ```
``` $ npm run android
$ npm run log ```
```
1. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html): 2. To see logging output, run the following command in another tab:
```
$ adb shell input keyevent 82
```
1. We recommend installing an [ESLint plugin in your editor](https://eslint.org/docs/user-guide/integrations#editors). There's an `.eslintrc` file in this project which will be used by the plugin to check your code for style errors and potential bugs. ```
$ npm run log
```
3. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html):
```
$ adb shell input keyevent 82
```
4. We recommend installing an [ESLint plugin in your editor](https://eslint.org/docs/user-guide/integrations#editors). There's an `.eslintrc` file in this project which will be used by the plugin to check your code for style errors and potential bugs.
#### 6. Run app on iOS
Minimum system requirements to run iOS app are as follows:
- MacOS 10.15.7 for Mac users
- Xcode 12.3 (I assume, that only command line tools might be enough)
1. Install XCode dependencies by running the following command from the root project directory:
```
$ cd ios && pod install && cd ..
```
2. To run app either open drip workspace ('drip.xcworkspace' file) with XCode and run "Build" or run the following command:
```
$ npm run ios
```
3. If you are building the app with XCode make sure you are running this as well:
`$ npm start`
### Troubleshooting ### Troubleshooting
#### [MacOS] Java problems #### [MacOS] Java problems
Make sure that you have Java 1.8 by running `java -version`. Make sure that you have Java 1.8 by running `java -version`.
If you don't have Java installed, or your Java version is different, the app may not work. You can try just using Android Studio's Java by prepending it to your `$PATH` in your shell profile: If you don't have Java installed, or your Java version is different, the app may not work. You can try just using Android Studio's Java by prepending it to your `$PATH` in your shell profile:
`$ export PATH="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin:${PATH}"`
```
$ export PATH="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin:${PATH}"
```
Now, `which java` should output `/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java`, and the correct Java version should be used. Now, `which java` should output `/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java`, and the correct Java version should be used.
#### [MacOS] Ninja #### [MacOS] Ninja
If `npm` says `CMake was unable to find a build program corresponding to "Ninja".`: If `npm` says `CMake was unable to find a build program corresponding to "Ninja".`:
``` `$ brew install ninja`
$ brew install ninja
```
### [MacOS] adb not on the path ### [MacOS] adb not on the path
If you get error messages about `adb` not being found on your path: If you get error messages about `adb` not being found on your path:
``` `$ ln -s ~/Library/Android/sdk/platform-tools/adb /usr/local/bin/adb`
$ ln -s ~/Library/Android/sdk/platform-tools/adb /usr/local/bin/adb
``` ### [MacOS] and XCode 12.5
If you run XCode 12.5, more likely you'll have problems building app for iOS. Please use the following fix: https://stackoverflow.com/a/67320887.
If you experience any further issues, please feel free to check out the following threads:
- [react-native run-ios build failure on XCode 12.5 beta](https://github.com/react-native-community/cli/issues/1365)
- [Xcode 12.5 troubleshooting guide (RN 0.61/0.62/0.63/0.64)](https://github.com/facebook/react-native/issues/31480)
### Clearing project cache
If you would like to clear project cache and/or re-install project libraries, you can run clear script as follows:
$ npm run clear
Script accepts the following options:
"none" - script will delete all caches and re-install project libraries,
"ios" - script will delete ios-related cache
"android" - script will delete android-related cache
"cache" - script will purge Watchman, Metrobundler, Pachager and React caches
"npm" - script will reinstall project libraries.
For example, if you would like to clear android part of the project and re-install project libraries, you can run the following command:
$ npm run clear android npm
## Tests ## Tests
### Unit tests ### Unit tests
You can run the tests with: You can run the tests with:
``` `$ npm test`
$ npm test
```
### End to end tests ### End to end tests
1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/master/package.json) under: 1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/master/package.json) under:
``` ```
{"detox": {"detox":
{"configurations": {"configurations":
{"name": "NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE"} {"name": "NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE"}
} }
} }
``` ```
2. Check if the current device is already installed on your machine. Go to `cd ~/Android/sdk/emulator/` or wherever you have Android installed on your machine. Here you can run `./emulator -list-avds` and compare the devices with the one you found in step 1. 2. Check if the current device is already installed on your machine. Go to `cd ~/Android/sdk/emulator/` or wherever you have Android installed on your machine. Here you can run `./emulator -list-avds` and compare the devices with the one you found in step 1.
3. Open Android Studio and go to -> Tools -> AVD manager -> `+Create virtual device` and select the device checked in the previous step 3. Open Android Studio and go to -> Tools -> AVD manager -> `+Create virtual device` and select the device checked in the previous step
4. Use the emulator on your machine to run it without heavy Android Studio, e.g. in `~/Android/Sdk/emulator` OR chose to run the emulator within Android Studio 4. Use the emulator on your machine to run it without heavy Android Studio, e.g. in `~/Android/Sdk/emulator` OR chose to run the emulator within Android Studio
4.1 Here run: `$ ./emulator -avd NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE` 4.1 Here run: `$ ./emulator -avd NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE`
4.2 You might need to specify the following environment variables in your zsh or bash file according to where you have it installed. You can find exact path in Android Studio (Android Studio Preferences → Appearance and Behavior → System Settings → Android SDK). After adding environment variables, you might need to restart your terminal or source the modified bash profile (i.e. "source ~/.bash_profile"). 4.2 You might need to specify the following environment variables in your zsh or bash file according to where you have it installed. You can find exact path in Android Studio (Android Studio Preferences → Appearance and Behavior → System Settings → Android SDK). After adding environment variables, you might need to restart your terminal or source the modified bash profile (i.e. "source ~/.bash_profile").
``` `export ANDROID_HOME="/home/myname/Android/Sdk" export ANDROID_SDK_ROOT="/home/myname/Android/Sdk" export ANDROID_AVD_HOME="/home/myname/.android/avd"`
export ANDROID_HOME="/home/myname/Android/Sdk"
export ANDROID_SDK_ROOT="/home/myname/Android/Sdk"
export ANDROID_AVD_HOME="/home/myname/.android/avd"
```
5. For the first time you need to get the app on the phone or if you run into this error: 5. For the first time you need to get the app on the phone or if you run into this error:
`'app-debug-androidTest.apk' could not be found` `'app-debug-androidTest.apk' could not be found`
--> open a new 2nd tab and run (in your drip folder): `cd android and ./gradlew assembleAndroidTest` --> open a new 2nd tab and run (in your drip folder): `cd android and ./gradlew assembleAndroidTest`
Otherwise just open a new 2nd tab to run (in your drip folder) `npm run android` Otherwise just open a new 2nd tab to run (in your drip folder) `npm run android`
6. Open a new 3rd tab to run `./node_modules/.bin/detox test -c android.emu.debug` 6. Open a new 3rd tab to run `./node_modules/.bin/detox test -c android.emu.debug`
Hopefully you see the magic happening clicking through the app and happy test results on your console :sun_with_face: ! Hopefully you see the magic happening clicking through the app and happy test results on your console :sun_with_face: !
## Debugging ## Debugging
In order to see logging output from the app, run `npm run log` in a separate terminal. You can output specific code you want to see, with: In order to see logging output from the app, run `npm run log` in a separate terminal. You can output specific code you want to see, with:
`console.log(theVariableIWantToSeeHere)` `console.log(theVariableIWantToSeeHere)`
or just a random string to check if this piece of code is actually running: or just a random string to check if this piece of code is actually running:
`console.log("HELLO")`. `console.log("HELLO")`.
## NFP rules ## NFP rules
More information about how the app calculates fertility status and bleeding predictions in the [wiki on Gitlab](https://gitlab.com/bloodyhealth/drip/wikis/home). More information about how the app calculates fertility status and bleeding predictions in the [wiki on Gitlab](https://gitlab.com/bloodyhealth/drip/wikis/home).
## Adding a new tracking icon ## Adding a new tracking icon
@@ -153,3 +196,7 @@ More information about how the app calculates fertility status and bleeding pred
$ react-native link $ react-native link
``` ```
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart. 5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
## Translation
We are using [Weblate](https://weblate.org/) as translation software.
+52 -12
View File
@@ -18,6 +18,9 @@ import com.android.build.OutputFile
* // the entry file for bundle generation * // the entry file for bundle generation
* entryFile: "index.android.js", * entryFile: "index.android.js",
* *
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode * // whether to bundle JS and assets in debug mode
* bundleInDebug: false, * bundleInDebug: false,
* *
@@ -73,7 +76,8 @@ import com.android.build.OutputFile
*/ */
project.ext.react = [ project.ext.react = [
entryFile: "index.js" entryFile: "index.js",
enableHermes: false, // clean and rebuild if changing
] ]
apply from: "../../node_modules/react-native/react.gradle" apply from: "../../node_modules/react-native/react.gradle"
@@ -94,6 +98,27 @@ def enableSeparateBuildPerCPUArchitecture = false
*/ */
def enableProguardInReleaseBuilds = false def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion rootProject.ext.buildToolsVersion
@@ -110,12 +135,18 @@ android {
versionCode 8 versionCode 8
versionName "1.2102.28" versionName "1.2102.28"
ndk { ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" abiFilters "armeabi-v7a", "x86"
} }
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
signingConfigs { signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
release { release {
if (project.hasProperty('DRIP_RELEASE_STORE_FILE')) { if (project.hasProperty('DRIP_RELEASE_STORE_FILE')) {
storeFile file(DRIP_RELEASE_STORE_FILE) storeFile file(DRIP_RELEASE_STORE_FILE)
@@ -134,7 +165,13 @@ android {
} }
} }
buildTypes { buildTypes {
debug {
signingConfig signingConfigs.debug
}
release { release {
// Caution! In production, you need to generate your own keystore file.
// see https://facebook.github.io/react-native/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release signingConfig signingConfigs.release
@@ -144,8 +181,8 @@ android {
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.outputs.each { output -> variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here: // For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits // https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a":3, "x86_64":4] def versionCodes = ["armeabi-v7a": 1, "x86": 2]
def abi = output.getFilter(OutputFile.ABI) def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride = output.versionCodeOverride =
@@ -156,18 +193,19 @@ android {
} }
dependencies { dependencies {
implementation project(':realm')
implementation project(':react-native-vector-icons')
implementation project(':react-native-share')
implementation project(':react-native-restart')
implementation project(':react-native-push-notification')
implementation project(':react-native-fs')
implementation project(':react-native-document-picker')
implementation project(':nodejs-mobile-react-native')
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:1.1.0'
implementation "com.facebook.react:react-native:+" // From node_modules implementation "com.facebook.react:react-native:+" // From node_modules
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
androidTestImplementation('com.wix:detox:+') { transitive = true } androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12' androidTestImplementation 'junit:junit:4.12'
} }
@@ -178,3 +216,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile from configurations.compile
into 'libs' into 'libs'
} }
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
Binary file not shown.
+16 -5
View File
@@ -10,6 +10,8 @@
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" /> <uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" />
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<permission <permission
android:name="${applicationId}.permission.C2D_MESSAGE" android:name="${applicationId}.permission.C2D_MESSAGE"
@@ -49,20 +51,29 @@
android:resource="@xml/filepaths" /> android:resource="@xml/filepaths" />
</provider> </provider>
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name" <meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="drip-notification"/> android:value="false"/>
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="notifications from drip"/>
<!-- Change the resource name to your App's accent color - or any other color you want --> <!-- Change the resource name to your App's accent color - or any other color you want -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color" <meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@android:color/white"/> android:resource="@android:color/white"/> <!-- or @android:color/{name} to use a standard color -->
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" /> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,12 +4,12 @@ import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity { public class MainActivity extends ReactActivity {
/** /**
* Returns the name of the main component registered from JavaScript. * Returns the name of the main component registered from JavaScript. This is used to schedule
* This is used to schedule rendering of the component. * rendering of the component.
*/ */
@Override @Override
protected String getMainComponentName() { protected String getMainComponentName() {
return "drip"; return "drip";
} }
} }
@@ -1,53 +1,39 @@
package com.drip; package com.drip;
import android.app.Application; import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.janeasystems.rn_nodejs_mobile.RNNodeJsMobilePackage;
import com.avishayil.rnrestart.ReactNativeRestartPackage;
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.rnfs.RNFSPackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import cl.json.RNSharePackage;
import cl.json.ShareApplication; import cl.json.ShareApplication;
import io.realm.react.RealmReactPackage;
import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader; import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List; import java.util.List;
public class MainApplication extends Application implements ReactApplication, ShareApplication { public class MainApplication extends Application implements ReactApplication, ShareApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { private final ReactNativeHost mReactNativeHost =
@Override new ReactNativeHost(this) {
public boolean getUseDeveloperSupport() { @Override
return BuildConfig.DEBUG; public boolean getUseDeveloperSupport() {
} return BuildConfig.DEBUG;
}
@Override @Override
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList( @SuppressWarnings("UnnecessaryLocalVariable")
new MainReactPackage(), List<ReactPackage> packages = new PackageList(this).getPackages();
new RNNodeJsMobilePackage(), // Packages that cannot be autolinked yet can be added manually here, for example:
new ReactNativeRestartPackage(), // packages.add(new MyReactNativePackage());
new ReactNativePushNotificationPackage(), return packages;
new VectorIconsPackage(), }
new RNFSPackage(),
new ReactNativeDocumentPicker(),
new RNSharePackage(),
new RealmReactPackage()
);
}
@Override @Override
protected String getJSMainModuleName() { protected String getJSMainModuleName() {
return "index"; return "index";
} }
}; };
@Override @Override
public ReactNativeHost getReactNativeHost() { public ReactNativeHost getReactNativeHost() {
@@ -58,6 +44,32 @@ public class MainApplication extends Application implements ReactApplication, Sh
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
SoLoader.init(this, /* native exopackage */ false); SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this); // Remove this line if you don't want Flipper enabled
}
/**
* Loads Flipper in React Native templates.
*
* @param context
*/
private static void initializeFlipper(Context context) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper");
aClass.getMethod("initializeFlipper", Context.class).invoke(null, context);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
} }
@Override @Override
@@ -3,6 +3,7 @@
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="android:textColor">#000000</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
+12 -5
View File
@@ -7,7 +7,7 @@ buildscript {
} }
ext.kotlinVersion = '1.3.10' ext.kotlinVersion = '1.3.10'
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.4.0' classpath("com.android.tools.build:gradle:3.4.2")
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@@ -18,16 +18,21 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
mavenLocal() mavenLocal()
jcenter()
maven { maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android" url("$rootDir/../node_modules/react-native/android")
} }
maven {
// Android JSC is installed from npm
url("$rootDir/../node_modules/jsc-android/dist")
}
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { maven {
url 'https://maven.google.com/' url 'https://maven.google.com/'
name 'Google' name 'Google'
} }
google()
maven { maven {
// All of Detox' artifacts are provided via the npm module // All of Detox' artifacts are provided via the npm module
url "$rootDir/../node_modules/detox/Detox-android" url "$rootDir/../node_modules/detox/Detox-android"
@@ -36,11 +41,13 @@ allprojects {
} }
ext { ext {
googlePlayServicesVersion = "+" // default: "+"
firebaseMessagingVersion = "21.1.0" // default: "+"
buildToolsVersion = "29.0.3" buildToolsVersion = "29.0.3"
minSdkVersion = 23 minSdkVersion = 23
compileSdkVersion = 29 compileSdkVersion = 29
targetSdkVersion = 29 targetSdkVersion = 29
supportLibVersion = "29.0.0"
} }
subprojects { subprojects {
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
-8
View File
@@ -1,8 +0,0 @@
keystore(
name = "debug",
properties = "debug.keystore.properties",
store = "debug.keystore",
visibility = [
"PUBLIC",
],
)
@@ -1,4 +0,0 @@
key.store=debug.keystore
key.alias=androiddebugkey
key.store.password=android
key.alias.password=android
+1 -16
View File
@@ -1,19 +1,4 @@
rootProject.name = 'drip' rootProject.name = 'drip'
include ':nodejs-mobile-react-native' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
project(':nodejs-mobile-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/nodejs-mobile-react-native/android')
include ':react-native-restart'
project(':react-native-restart').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-restart/android')
include ':react-native-push-notification'
project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-document-picker'
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android')
include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':app' include ':app'
+3 -3
View File
@@ -1,4 +1,4 @@
{ {
"name": "drip", "name": "drip.",
"displayName": "drip" "displayName": "drip."
} }
+155
View File
@@ -0,0 +1,155 @@
import React from 'react'
import { ScrollView, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import moment from 'moment'
import { connect } from 'react-redux'
import { navigate } from '../slices/navigation'
import { getDate, setDate } from '../slices/date'
import AppText from './common/app-text'
import Button from './common/button'
import cycleModule from '../lib/cycle'
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
import { determinePredictionText, formatWithOrdinalSuffix } from './helpers/home'
import { Colors, Fonts, Sizes, Spacing } from '../styles'
import { LocalDate } from 'js-joda'
import { useTranslation } from 'react-i18next'
const Home = ({ navigate, setDate }) => {
const { t } = useTranslation()
function navigateToCycleDayView() {
setDate(todayDateString)
navigate('CycleDay')
}
const todayDateString = LocalDate.now().toString()
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
const cycleDayNumber = getCycleDayNumber(todayDateString)
const { status, phase, statusText } =
getFertilityStatusForDay(todayDateString)
const prediction = determinePredictionText(getPredictedMenses(), t)
const cycleDayText = cycleDayNumber ? formatWithOrdinalSuffix(cycleDayNumber) : ''
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
>
<AppText style={styles.title}>{moment().format("MMM Do YYYY")}</AppText>
{cycleDayNumber &&
<View style={styles.line}>
<AppText style={styles.whiteSubtitle}>{cycleDayText}</AppText>
<AppText style={styles.turquoiseText}>{t('labels.home.cycleDay')}</AppText>
</View>
}
{phase &&
<View style={styles.line}>
<AppText style={styles.whiteSubtitle}>
{formatWithOrdinalSuffix(phase)}
</AppText>
<AppText style={styles.turquoiseText}>
{t('labels.home.cyclePhase')}
</AppText>
<AppText style={styles.turquoiseText}>{status}</AppText>
<Asterisk />
</View>
}
<View style={styles.line}>
<AppText style={styles.turquoiseText}>{prediction}</AppText>
</View>
<Button isCTA isSmall={false} onPress={navigateToCycleDayView}>
{t('labels.home.addDataForToday')}
</Button>
{phase && (
<View style={styles.asteriskLine}>
<Asterisk />
<AppText linkStyle={styles.whiteText} style={styles.greyText}>
{statusText}
</AppText>
</View>
)}
</ScrollView>
)
}
const Asterisk = () => {
return <AppText style={styles.asterisk}>*</AppText>
}
const styles = StyleSheet.create({
asterisk: {
color: Colors.orange,
},
container: {
backgroundColor: Colors.purple,
flex: 1,
},
contentContainer: {
padding: Spacing.base,
paddingTop: 0,
},
line: {
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
asteriskLine: {
flexDirection: 'row',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
title: {
color: Colors.purpleLight,
fontFamily: Fonts.bold,
fontSize: Sizes.huge,
marginVertical: Spacing.small,
},
turquoiseText: {
color: Colors.turquoise,
fontSize: Sizes.subtitle,
},
whiteSubtitle: {
color: 'white',
fontSize: Sizes.subtitle,
},
whiteText: {
color: 'white',
},
greyText: {
color: Colors.greyLight,
paddingLeft: Spacing.base,
}
})
const mapStateToProps = (state) => {
return ({
date: getDate(state),
})
}
const mapDispatchToProps = (dispatch) => {
return ({
navigate: (page) => dispatch(navigate(page)),
setDate: (date) => dispatch(setDate(date)),
})
}
Home.propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Home)
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { BackHandler, StyleSheet, View } from 'react-native' import { BackHandler, StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next'
import AppPage from './common/app-page' import AppPage from './common/app-page'
import AppText from './common/app-text' import AppText from './common/app-text'
@@ -9,28 +10,27 @@ import Segment from './common/segment'
import { saveLicenseFlag } from '../local-storage' import { saveLicenseFlag } from '../local-storage'
import { shared } from '../i18n/en/labels'
import settingsLabels from '../i18n/en/settings'
import { Containers } from '../styles' import { Containers } from '../styles'
const labels = settingsLabels.license
export default function License({ setLicense }) { export default function License({ setLicense }) {
const onAcceptLicense = async () => { const onAcceptLicense = async () => {
await saveLicenseFlag() await saveLicenseFlag()
setLicense() setLicense()
} }
const { t } = useTranslation()
const currentYear = new Date().getFullYear()
return ( return (
<AppPage testID="licensePage"> <AppPage testID="licensePage">
<Segment last testID="test" title={labels.title}> <Segment last testID="test" title={t("settings.license.title")}>
<AppText testID="test">{labels.text}</AppText> <AppText testID="test">{t("settings.license.text", { currentYear })}</AppText>
<View style={styles.container}> <View style={styles.container}>
<Button onPress={BackHandler.exitApp} testID="licenseCancelButton"> <Button onPress={BackHandler.exitApp} testID="licenseCancelButton">
{shared.cancel} {t("labels.shared.cancel")}
</Button> </Button>
<Button isCTA onPress={onAcceptLicense} testID="licenseOkButton"> <Button isCTA onPress={onAcceptLicense} testID="licenseOkButton">
{shared.ok} {t("labels.shared.ok")}
</Button> </Button>
</View> </View>
</Segment> </Segment>
+25 -11
View File
@@ -1,16 +1,18 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { StyleSheet, View } from 'react-native'
import { Provider } from 'react-redux'
import nodejs from 'nodejs-mobile-react-native' import nodejs from 'nodejs-mobile-react-native'
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage' import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
import { openDb } from '../db' import { openDb } from '../db'
import App from './app' import App from './app'
import PasswordPrompt from './password-prompt'
import License from './license'
import AppLoadingView from './common/app-loading' import AppLoadingView from './common/app-loading'
import AppStatusBar from './common/app-status-bar'
import License from './License'
import PasswordPrompt from './password-prompt'
import store from "../store" import store from '../store'
import { Provider } from 'react-redux'
export default class AppWrapper extends Component { export default class AppWrapper extends Component {
constructor() { constructor() {
@@ -49,7 +51,7 @@ export default class AppWrapper extends Component {
enableShowLicenseAgreement = () => { enableShowLicenseAgreement = () => {
this.setState({ this.setState({
shouldShowLicenseAgreement: true, shouldShowLicenseAgreement: true,
isCheckingLicenseAgreement: false isCheckingLicenseAgreement: false,
}) })
} }
@@ -60,7 +62,7 @@ export default class AppWrapper extends Component {
enableShowApp = () => { enableShowApp = () => {
this.setState({ this.setState({
shouldShowApp: true, shouldShowApp: true,
shouldShowPasswordPrompt: false shouldShowPasswordPrompt: false,
}) })
} }
@@ -77,14 +79,26 @@ export default class AppWrapper extends Component {
if (isCheckingLicenseAgreement) { if (isCheckingLicenseAgreement) {
initialView = <AppLoadingView /> initialView = <AppLoadingView />
} else if (shouldShowLicenseAgreement) { } else if (shouldShowLicenseAgreement) {
initialView = <License setLicense={this.disableShowLicenseAgreement}/> initialView = <License setLicense={this.disableShowLicenseAgreement} />
} else if (shouldShowPasswordPrompt) { } else if (shouldShowPasswordPrompt) {
initialView = <PasswordPrompt enableShowApp={this.enableShowApp} /> initialView = <PasswordPrompt enableShowApp={this.enableShowApp} />
} else if (shouldShowApp) { } else if (shouldShowApp) {
initialView = <App /> initialView = <App restartApp={() => this.checkDbPasswordSet()} />
} }
return <Provider store={store}>{initialView}</Provider> return (
<Provider store={store}>
<View style={styles.container}>
<AppStatusBar />
{initialView}
</View>
</Provider>
)
} }
} }
const styles = StyleSheet.create({
container: {
flex: 1,
},
})
+12 -15
View File
@@ -17,12 +17,12 @@ import setupNotifications from '../lib/notifications'
import { getCycleDay, closeDb } from '../db' import { getCycleDay, closeDb } from '../db'
class App extends Component { class App extends Component {
static propTypes = { static propTypes = {
date: PropTypes.string, date: PropTypes.string,
navigation: PropTypes.object.isRequired, navigation: PropTypes.object.isRequired,
navigate: PropTypes.func, navigate: PropTypes.func,
goBack: PropTypes.func, goBack: PropTypes.func,
restartApp: PropTypes.func,
} }
constructor(props) { constructor(props) {
@@ -54,7 +54,7 @@ class App extends Component {
} }
render() { render() {
const { date, navigation, goBack } = this.props const { date, navigation, goBack, restartApp } = this.props
const { currentPage } = navigation const { currentPage } = navigation
if (!currentPage) { if (!currentPage) {
@@ -80,8 +80,8 @@ class App extends Component {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header { ...headerProps } /> <Header {...headerProps} />
<Page { ...pageProps } /> <Page {...pageProps} restartApp={restartApp} />
<Menu /> <Menu />
</View> </View>
) )
@@ -90,25 +90,22 @@ class App extends Component {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1 flex: 1,
} },
}) })
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return({ return {
date: getDate(state), date: getDate(state),
navigation: getNavigation(state) navigation: getNavigation(state),
}) }
} }
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return({ return {
navigate: (page) => dispatch(navigate(page)), navigate: (page) => dispatch(navigate(page)),
goBack: () => dispatch(goBack()), goBack: () => dispatch(goBack()),
}) }
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(App)
mapStateToProps,
mapDispatchToProps
)(App)
+18 -6
View File
@@ -1,6 +1,13 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ActivityIndicator, FlatList, Dimensions, StyleSheet, View } from 'react-native' import {
ActivityIndicator,
Dimensions,
FlatList,
PixelRatio,
StyleSheet,
View
} from 'react-native'
import AppLoadingView from '../common/app-loading' import AppLoadingView from '../common/app-loading'
import AppPage from '../common/app-page' import AppPage from '../common/app-page'
@@ -22,9 +29,10 @@ import { makeColumnInfo, nfpLines } from '../helpers/chart'
import { import {
CHART_COLUMN_WIDTH, CHART_COLUMN_WIDTH,
SYMPTOMS, CHART_GRID_LINE_HORIZONTAL_WIDTH,
CHART_SYMPTOM_HEIGHT_RATIO, CHART_SYMPTOM_HEIGHT_RATIO,
CHART_XAXIS_HEIGHT_RATIO CHART_XAXIS_HEIGHT_RATIO,
SYMPTOMS
} from '../../config' } from '../../config'
import { shared } from '../../i18n/en/labels' import { shared } from '../../i18n/en/labels'
import { Colors, Spacing } from '../../styles' import { Colors, Spacing } from '../../styles'
@@ -105,9 +113,13 @@ class CycleChart extends Component {
this.xAxisHeight = height * 0.7 * CHART_XAXIS_HEIGHT_RATIO this.xAxisHeight = height * 0.7 * CHART_XAXIS_HEIGHT_RATIO
const remainingHeight = height * 0.7 - this.xAxisHeight const remainingHeight = height * 0.7 - this.xAxisHeight
this.symptomHeight = remainingHeight * CHART_SYMPTOM_HEIGHT_RATIO this.symptomHeight = PixelRatio.roundToNearestPixel(
this.symptomRowHeight = this.symptomRowSymptoms.length * remainingHeight
this.symptomHeight * CHART_SYMPTOM_HEIGHT_RATIO
)
this.symptomRowHeight = PixelRatio.roundToNearestPixel(
this.symptomRowSymptoms.length * this.symptomHeight
) + CHART_GRID_LINE_HORIZONTAL_WIDTH
this.columnHeight = remainingHeight - this.symptomRowHeight this.columnHeight = remainingHeight - this.symptomRowHeight
const chartHeight = this.shouldShowTemperatureColumn ? const chartHeight = this.shouldShowTemperatureColumn ?
height * 0.7 : (this.symptomRowHeight + this.xAxisHeight) height * 0.7 : (this.symptomRowHeight + this.xAxisHeight)
+5 -8
View File
@@ -7,7 +7,7 @@ import AppText from '../common/app-text'
import cycleModule from '../../lib/cycle' import cycleModule from '../../lib/cycle'
import { getOrdinalSuffix } from '../helpers/home' import { getOrdinalSuffix } from '../helpers/home'
import { Containers, Typography, Sizes } from '../../styles' import { Typography, Sizes } from '../../styles'
const CycleDayLabel = ({ height, date }) => { const CycleDayLabel = ({ height, date }) => {
const cycleDayNumber = cycleModule().getCycleDayNumber(date) const cycleDayNumber = cycleModule().getCycleDayNumber(date)
@@ -24,11 +24,11 @@ const CycleDayLabel = ({ height, date }) => {
<AppText style={styles.text}> <AppText style={styles.text}>
{isFirstDayOfMonth ? momentDate.format('MMM') : dayOfMonth} {isFirstDayOfMonth ? momentDate.format('MMM') : dayOfMonth}
</AppText> </AppText>
{!isFirstDayOfMonth && {!isFirstDayOfMonth && (
<AppText style={styles.textLight}> <AppText style={styles.textLight}>
{getOrdinalSuffix(dayOfMonth)} {getOrdinalSuffix(dayOfMonth)}
</AppText> </AppText>
} )}
</View> </View>
</View> </View>
) )
@@ -45,15 +45,12 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end', justifyContent: 'flex-end',
left: 4, left: 4,
}, },
containerRow: {
...Containers.rowContainer
},
text: { text: {
...Typography.label, ...Typography.label,
fontSize: Sizes.small, fontSize: Sizes.small,
}, },
textBold: { textBold: {
...Typography.labelBold ...Typography.labelBold,
}, },
textLight: { textLight: {
...Typography.labelLight, ...Typography.labelLight,
@@ -62,7 +59,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
alignItems: 'center', alignItems: 'center',
} },
}) })
export default CycleDayLabel export default CycleDayLabel
+2 -1
View File
@@ -105,7 +105,8 @@ class DayColumn extends Component {
/> />
{ symptomRowSymptoms.map((symptom, i) => { { symptomRowSymptoms.map((symptom, i) => {
const hasSymptomData = this.data.hasOwnProperty(symptom) const hasSymptomData =
Object.prototype.hasOwnProperty.call(this.data, symptom)
return ( return (
<SymptomCell <SymptomCell
index={i} index={i}
+3 -4
View File
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import { Sizes } from '../../styles' import { Sizes } from '../../styles'
import { CHART_TICK_WIDTH } from '../../config'
const Tick = ({ yPosition, height, isBold, shouldShowLabel, label }) => { const Tick = ({ yPosition, height, isBold, shouldShowLabel, label }) => {
const top = yPosition - height / 2 const top = yPosition - height / 2
@@ -28,16 +29,14 @@ Tick.propTypes = {
const text = { const text = {
lineHeight: Sizes.base, textAlign: 'right',
right: 4,
textAlign: 'right'
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
justifyContent: 'center', justifyContent: 'center',
position: 'absolute', position: 'absolute',
right: 0, right: 0,
width: 40 width: CHART_TICK_WIDTH
}, },
textBold: { textBold: {
fontSize: Sizes.base, fontSize: Sizes.base,
+29
View File
@@ -0,0 +1,29 @@
import React from 'react'
import { SafeAreaView, StatusBar, StyleSheet, View } from 'react-native'
import { Colors } from '../../styles'
import { STATUSBAR_HEIGHT } from '../../config'
const AppStatusBar = () => (
<View style={styles.statusBar}>
<SafeAreaView>
<StatusBar
backgroundColor={Colors.purple}
barStyle="light-content"
translucent
/>
</SafeAreaView>
</View>
)
const styles = StyleSheet.create({
container: {
flex: 1,
},
statusBar: {
backgroundColor: Colors.purple,
height: STATUSBAR_HEIGHT,
}
})
export default AppStatusBar
+1
View File
@@ -38,6 +38,7 @@ const styles = StyleSheet.create({
marginTop: Spacing.base, marginTop: Spacing.base,
minWidth: '80%', minWidth: '80%',
paddingHorizontal: Spacing.base, paddingHorizontal: Spacing.base,
paddingVertical: Spacing.tiny,
...Typography.mainText ...Typography.mainText
} }
}) })
+20
View File
@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import { StyleSheet, View } from 'react-native'
const ButtonRow = ({ children }) => {
return <View style={styles.container}>{children}</View>
}
ButtonRow.propTypes = {
children: PropTypes.node.isRequired,
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
},
})
export default ButtonRow
+15 -13
View File
@@ -5,7 +5,7 @@ import { StyleSheet, TouchableOpacity } from 'react-native'
import AppIcon from './app-icon' import AppIcon from './app-icon'
import AppText from './app-text' import AppText from './app-text'
import { Colors, Fonts, Spacing } from '../../styles' import { Colors, Fonts, Sizes, Spacing } from '../../styles'
const Button = ({ const Button = ({
children, children,
@@ -39,49 +39,51 @@ Button.propTypes = {
isCTA: PropTypes.bool, isCTA: PropTypes.bool,
isSmall: PropTypes.bool, isSmall: PropTypes.bool,
onPress: PropTypes.func, onPress: PropTypes.func,
testID: PropTypes.string testID: PropTypes.string,
} }
Button.defaultProps = { Button.defaultProps = {
isSmall: true isSmall: true,
} }
const text = { const text = {
padding: Spacing.base, padding: Spacing.base,
textTransform: 'uppercase' textTransform: 'uppercase',
} }
const textSmall = { const textSmall = {
fontSize: Fonts.small, fontSize: Sizes.small,
padding: Spacing.small, padding: Spacing.small,
textTransform: 'uppercase' textTransform: 'uppercase',
} }
const button = { const button = {
alignItems: 'center', alignItems: 'center',
alignSelf: 'center',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
margin: Spacing.base, marginTop: Spacing.base,
minWidth: '15%' paddingHorizontal: Spacing.tiny,
minWidth: '15%',
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
regular: { regular: {
...button ...button,
}, },
cta: { cta: {
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
borderRadius: 25, borderRadius: 25,
...button ...button,
}, },
buttonTextBold: { buttonTextBold: {
color: 'white', color: 'white',
fontFamily: Fonts.bold fontFamily: Fonts.bold,
}, },
buttonTextRegular: { buttonTextRegular: {
color: Colors.greyDark, color: Colors.greyDark,
fontFamily: Fonts.main fontFamily: Fonts.main,
} },
}) })
export default Button export default Button
+8 -5
View File
@@ -4,29 +4,32 @@ import { StyleSheet, TouchableOpacity } from 'react-native'
import AppIcon from './app-icon' import AppIcon from './app-icon'
import { HIT_SLOP} from '../../config'
import { Colors, Sizes } from '../../styles' import { Colors, Sizes } from '../../styles'
const CloseIcon = ({ onClose, ...props }) => { const CloseIcon = ({ onClose, color, ...props }) => {
return ( return (
<TouchableOpacity <TouchableOpacity
hitSlop={HIT_SLOP}
onPress={onClose} onPress={onClose}
style={styles.container} style={styles.container}
{...props} {...props}
> >
<AppIcon name='cross' color={Colors.orange} /> <AppIcon name='cross' color={color ? color : Colors.orange} />
</TouchableOpacity> </TouchableOpacity>
) )
} }
CloseIcon.propTypes = { CloseIcon.propTypes = {
onClose: PropTypes.func.isRequired onClose: PropTypes.func.isRequired,
color: PropTypes.string
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
marginBottom: Sizes.base marginBottom: Sizes.base,
} }
}) })
export default CloseIcon export default CloseIcon
+4 -2
View File
@@ -57,10 +57,11 @@ const styles = StyleSheet.create({
accentOrange: { accentOrange: {
...Typography.accentOrange, ...Typography.accentOrange,
fontSize: Sizes.small, fontSize: Sizes.small,
margin: Sizes.tiny,
}, },
accentPurpleBig: { accentPurpleBig: {
...Typography.accentPurpleBig, ...Typography.accentPurpleBig,
marginRight: Spacing.small, marginRight: Spacing.tiny
}, },
cellLeft: { cellLeft: {
alignItems: 'flex-end', alignItems: 'flex-end',
@@ -68,12 +69,13 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
cellRight: { cellRight: {
flex: 6, flex: 5,
justifyContent: 'center', justifyContent: 'center',
}, },
row: { row: {
flexDirection: 'row', flexDirection: 'row',
marginBottom: Spacing.tiny, marginBottom: Spacing.tiny,
marginLeft: Spacing.tiny
} }
}) })
+28 -40
View File
@@ -1,6 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { StyleSheet, View, TouchableOpacity } from 'react-native' import { StyleSheet, View, TouchableOpacity } from 'react-native'
import { scale } from 'react-native-size-matters'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import DripIcon from '../../assets/drip-icons' import DripIcon from '../../assets/drip-icons'
@@ -14,7 +15,6 @@ import { Colors, Sizes, Spacing } from '../../styles'
import { headerTitles as symptomTitles } from '../../i18n/en/labels' import { headerTitles as symptomTitles } from '../../i18n/en/labels'
class SymptomBox extends Component { class SymptomBox extends Component {
static propTypes = { static propTypes = {
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
isSymptomEdited: PropTypes.bool, isSymptomEdited: PropTypes.bool,
@@ -32,7 +32,7 @@ class SymptomBox extends Component {
super(props) super(props)
this.state = { this.state = {
isSymptomEdited: props.isSymptomEdited isSymptomEdited: props.isSymptomEdited,
} }
} }
@@ -57,24 +57,24 @@ class SymptomBox extends Component {
const iconName = `drip-icon-${symptom}` const iconName = `drip-icon-${symptom}`
const symptomNameStyle = [ const symptomNameStyle = [
styles.symptomName, styles.symptomName,
(isSymptomDisabled && styles.symptomNameDisabled), isSymptomDisabled && styles.symptomNameDisabled,
(isExcluded && styles.symptomNameExcluded) isExcluded && styles.symptomNameExcluded,
] ]
const textStyle = [ const textStyle = [
styles.text, styles.text,
(isSymptomDisabled && styles.textDisabled), isSymptomDisabled && styles.textDisabled,
(isExcluded && styles.textExcluded) isExcluded && styles.textExcluded,
] ]
return ( return (
<React.Fragment> <React.Fragment>
{isSymptomEdited && {isSymptomEdited && (
<SymptomEditView <SymptomEditView
symptom={symptom} symptom={symptom}
symptomData={symptomData} symptomData={symptomData}
onClose={this.onFinishEditing} onClose={this.onFinishEditing}
/> />
} )}
<TouchableOpacity <TouchableOpacity
disabled={isSymptomDisabled} disabled={isSymptomDisabled}
@@ -86,17 +86,17 @@ class SymptomBox extends Component {
color={iconColor} color={iconColor}
isActive={!isSymptomDisabled} isActive={!isSymptomDisabled}
name={iconName} name={iconName}
size={40} size={Sizes.icon}
/> />
<View style={styles.textContainer}> <View style={styles.textContainer}>
<AppText style={symptomNameStyle}> <AppText style={symptomNameStyle}>
{symptomTitles[symptom].toLowerCase()} {symptomTitles[symptom].toLowerCase()}
</AppText> </AppText>
{symptomDataToDisplay && {symptomDataToDisplay && (
<AppText style={textStyle} numberOfLines={4}> <AppText style={textStyle} numberOfLines={4}>
{symptomDataToDisplay} {symptomDataToDisplay}
</AppText> </AppText>
} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</React.Fragment> </React.Fragment>
@@ -104,65 +104,53 @@ class SymptomBox extends Component {
} }
} }
const hint = {
fontSize: Sizes.small,
fontStyle: 'italic'
}
const main = {
fontSize: Sizes.base,
height: Sizes.base * 2,
lineHeight: Sizes.base,
marginBottom: (-1) * Sizes.tiny,
textAlignVertical: 'center'
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
alignItems: 'center', alignItems: 'center',
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: 10, borderRadius: scale(10),
elevation: 4, elevation: 4,
flexDirection: 'row', flexDirection: 'row',
height: 110, height: scale(110),
marginBottom: Spacing.base, marginBottom: Spacing.base,
paddingHorizontal: Spacing.small, paddingHorizontal: Spacing.small,
paddingVertical: Spacing.base, paddingVertical: Spacing.base,
width: Spacing.symptomTileWidth width: Spacing.symptomTileWidth,
}, },
symptomName: { symptomName: {
paddingTop: Sizes.tiny,
color: Colors.purple, color: Colors.purple,
...main fontSize: Sizes.base,
lineHeight: Sizes.base,
}, },
symptomNameDisabled: { symptomNameDisabled: {
color: Colors.grey color: Colors.grey,
}, },
symptomNameExcluded: { symptomNameExcluded: {
color: Colors.greyDark, color: Colors.greyDark,
}, },
textContainer: { textContainer: {
flexDirection: 'column', flexDirection: 'column',
marginLeft: Spacing.small, justifyContent: 'center',
maxWidth: Spacing.textWidth marginLeft: Spacing.tiny,
maxWidth: Spacing.textWidth,
}, },
text: { text: {
...hint fontSize: Sizes.small,
fontStyle: 'italic',
}, },
textDisabled: { textDisabled: {
color: Colors.greyLight color: Colors.greyLight,
}, },
textExcluded: { textExcluded: {
color: Colors.grey, color: Colors.grey,
} },
}) })
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return({ return {
date: getDate(state), date: getDate(state),
}) }
} }
export default connect( export default connect(mapStateToProps, null)(SymptomBox)
mapStateToProps,
null,
)(SymptomBox)
+83 -76
View File
@@ -1,6 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native' import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'
import { connect } from 'react-redux'
import AppModal from '../common/app-modal' import AppModal from '../common/app-modal'
import AppSwitch from '../common/app-switch' import AppSwitch from '../common/app-switch'
@@ -13,21 +14,20 @@ import SelectBoxGroup from './select-box-group'
import SelectTabGroup from './select-tab-group' import SelectTabGroup from './select-tab-group'
import Temperature from './temperature' import Temperature from './temperature'
import { connect } from 'react-redux'
import { getDate } from '../../slices/date' import { getDate } from '../../slices/date'
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day' import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
import { showToast } from '../helpers/general'
import { shared as sharedLabels } from '../../i18n/en/labels' import { shared as sharedLabels } from '../../i18n/en/labels'
import info from '../../i18n/en/symptom-info' import info from '../../i18n/en/symptom-info'
import { Colors, Containers, Sizes, Spacing } from '../../styles' import { Colors, Containers, Sizes, Spacing } from '../../styles'
class SymptomEditView extends Component { class SymptomEditView extends Component {
static propTypes = { static propTypes = {
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
symptom: PropTypes.string.isRequired, symptom: PropTypes.string.isRequired,
symptomData: PropTypes.object symptomData: PropTypes.object,
} }
constructor(props) { constructor(props) {
@@ -48,7 +48,7 @@ class SymptomEditView extends Component {
shouldShowInfo: false, shouldShowInfo: false,
shouldShowNote, shouldShowNote,
shouldBoxGroup, shouldBoxGroup,
shouldTabGroup shouldTabGroup,
} }
} }
@@ -84,18 +84,20 @@ class SymptomEditView extends Component {
onRemove = () => { onRemove = () => {
this.saveData(true) this.saveData(true)
showToast(sharedLabels.dataDeleted)
this.props.onClose() this.props.onClose()
} }
onSave = () => { onSave = () => {
this.saveData() this.saveData()
showToast(sharedLabels.dataSaved)
this.props.onClose() this.props.onClose()
} }
onSaveTemperature = (value, field) => { onSaveTemperature = (value, field) => {
const data = this.getParsedData() const data = this.getParsedData()
const dataToSave = field === 'value' const dataToSave =
? { [field]: Number(value) } : { [field]: value } field === 'value' ? { [field]: Number(value) } : { [field]: value }
Object.assign(data, { ...dataToSave }) Object.assign(data, { ...dataToSave })
this.setState({ data }) this.setState({ data })
@@ -103,10 +105,10 @@ class SymptomEditView extends Component {
onSelectBox = (key) => { onSelectBox = (key) => {
const data = this.getParsedData() const data = this.getParsedData()
if (key === "other") { if (key === 'other') {
Object.assign(data, { Object.assign(data, {
note: null, note: null,
[key]: !this.state.data[key] [key]: !this.state.data[key],
}) })
} else { } else {
Object.assign(data, { [key]: !this.state.data[key] }) Object.assign(data, { [key]: !this.state.data[key] })
@@ -115,7 +117,7 @@ class SymptomEditView extends Component {
this.setState({ data }) this.setState({ data })
} }
onSelectBoxNote= (value) => { onSelectBoxNote = (value) => {
const data = this.getParsedData() const data = this.getParsedData()
Object.assign(data, { note: value !== '' ? value : null }) Object.assign(data, { note: value !== '' ? value : null })
@@ -135,94 +137,102 @@ class SymptomEditView extends Component {
save[symptom](data, date, shouldDeleteData) save[symptom](data, date, shouldDeleteData)
} }
closeView = () => {
const { onClose } = this.props
showToast(sharedLabels.dataSaved)
onClose()
}
render() { render() {
const { onClose, symptom } = this.props const { symptom } = this.props
const { data, const {
data,
shouldShowExclude, shouldShowExclude,
shouldShowInfo, shouldShowInfo,
shouldShowNote, shouldShowNote,
shouldBoxGroup, shouldBoxGroup,
shouldTabGroup shouldTabGroup,
} = this.state } = this.state
const iconName = shouldShowInfo ? "chevron-down" : "chevron-up" const iconName = shouldShowInfo ? 'chevron-up' : 'chevron-down'
const noteText = symptom === 'note' ? data.value : data.note const noteText = symptom === 'note' ? data.value : data.note
return ( return (
<AppModal onClose={onClose}> <AppModal onClose={this.closeView}>
<ScrollView <ScrollView
contentContainerStyle={styles.modalContainer} contentContainerStyle={styles.modalContainer}
style={styles.modalWindow} style={styles.modalWindow}
> >
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<CloseIcon onClose={onClose} /> <CloseIcon onClose={this.closeView} />
</View> </View>
{symptom === 'temperature' && {symptom === 'temperature' && (
<Temperature <Temperature
data={data} data={data}
save={(value, field) => this.onSaveTemperature(value, field)} save={(value, field) => this.onSaveTemperature(value, field)}
/> />
} )}
{shouldTabGroup && symtomPage[symptom].selectTabGroups.map(group => { {shouldTabGroup &&
return ( symtomPage[symptom].selectTabGroups.map((group) => {
<Segment key={group.key} style={styles.segmentBorder}> return (
<AppText style={styles.title}>{group.title}</AppText> <Segment key={group.key} style={styles.segmentBorder}>
<SelectTabGroup <AppText style={styles.title}>{group.title}</AppText>
activeButton={data[group.key]} <SelectTabGroup
buttons={group.options} activeButton={data[group.key]}
onSelect={value => this.onSelectTab(group, value)} buttons={group.options}
/> onSelect={(value) => this.onSelectTab(group, value)}
</Segment>
)
})
}
{shouldBoxGroup && symtomPage[symptom].selectBoxGroups.map(group => {
const isOtherSelected =
data['other'] !== null
&& data['other'] !== false
&& Object.keys(group.options).includes('other')
return (
<Segment key={group.key} style={styles.segmentBorder} >
<AppText style={styles.title}>{group.title}</AppText>
<SelectBoxGroup
labels={group.options}
onSelect={value => this.onSelectBox(value)}
optionsState={data}
/>
{isOtherSelected &&
<AppTextInput
multiline={true}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={value => this.onSelectBoxNote(value)}
/> />
} </Segment>
</Segment> )
) })}
}) {shouldBoxGroup &&
} symtomPage[symptom].selectBoxGroups.map((group) => {
{shouldShowExclude && const isOtherSelected =
<Segment style={styles.segmentBorder} > data['other'] !== null &&
data['other'] !== false &&
Object.keys(group.options).includes('other')
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectBoxGroup
labels={group.options}
onSelect={(value) => this.onSelectBox(value)}
optionsState={data}
/>
{isOtherSelected && (
<AppTextInput
multiline={true}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={(value) => this.onSelectBoxNote(value)}
/>
)}
</Segment>
)
})}
{shouldShowExclude && (
<Segment style={styles.segmentBorder}>
<AppSwitch <AppSwitch
onToggle={this.onExcludeToggle} onToggle={this.onExcludeToggle}
text={symtomPage[symptom].excludeText} text={symtomPage[symptom].excludeText}
value={data.exclude} value={data.exclude}
/> />
</Segment> </Segment>
} )}
{shouldShowNote && {shouldShowNote && (
<Segment style={styles.segmentBorder} > <Segment style={styles.segmentBorder}>
<AppText>{symtomPage[symptom].note}</AppText> <AppText>{symtomPage[symptom].note}</AppText>
<AppTextInput <AppTextInput
multiline={true} multiline={true}
numberOfLines={3} numberOfLines={3}
onChangeText={this.onEditNote} onChangeText={this.onEditNote}
placeholder={sharedLabels.enter} placeholder={sharedLabels.enter}
testID='noteInput' testID="noteInput"
value={noteText !== null ? noteText : ''} value={noteText !== null ? noteText : ''}
/> />
</Segment> </Segment>
} )}
<View style={styles.buttonsContainer}> <View style={styles.buttonsContainer}>
<Button iconName={iconName} isSmall onPress={this.onPressLearnMore}> <Button iconName={iconName} isSmall onPress={this.onPressLearnMore}>
{sharedLabels.learnMore} {sharedLabels.learnMore}
@@ -234,11 +244,11 @@ class SymptomEditView extends Component {
{sharedLabels.save} {sharedLabels.save}
</Button> </Button>
</View> </View>
{shouldShowInfo && {shouldShowInfo && (
<Segment last style={styles.segmentBorder} > <Segment last style={styles.segmentBorder}>
<AppText>{info[symptom].text}</AppText> <AppText>{info[symptom].text}</AppText>
</Segment> </Segment>
} )}
</ScrollView> </ScrollView>
</AppModal> </AppModal>
) )
@@ -247,7 +257,7 @@ class SymptomEditView extends Component {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
buttonsContainer: { buttonsContainer: {
...Containers.rowContainer ...Containers.rowContainer,
}, },
headerContainer: { headerContainer: {
flexDirection: 'row', flexDirection: 'row',
@@ -265,23 +275,20 @@ const styles = StyleSheet.create({
marginVertical: Sizes.huge * 2, marginVertical: Sizes.huge * 2,
position: 'absolute', position: 'absolute',
minHeight: '40%', minHeight: '40%',
maxHeight: Dimensions.get('window').height * 0.7 maxHeight: Dimensions.get('window').height * 0.7,
}, },
segmentBorder: { segmentBorder: {
borderBottomColor: Colors.greyLight borderBottomColor: Colors.greyLight,
}, },
title: { title: {
fontSize: Sizes.subtitle fontSize: Sizes.subtitle,
} },
}) })
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return({ return {
date: getDate(state), date: getDate(state),
}) }
} }
export default connect( export default connect(mapStateToProps, null)(SymptomEditView)
mapStateToProps,
null,
)(SymptomEditView)
+4 -1
View File
@@ -24,6 +24,9 @@ const SymptomPageTitle = ({
reloadSymptomData(nextDay) reloadSymptomData(nextDay)
setDate(nextDay) setDate(nextDay)
} }
const formattedTitle = title.length > 21
? title.substring(0, 18) + '...'
: title
return ( return (
<View style={styles.container}> <View style={styles.container}>
@@ -31,7 +34,7 @@ const SymptomPageTitle = ({
<AppIcon name='chevron-left' color={Colors.orange}/> <AppIcon name='chevron-left' color={Colors.orange}/>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.textContainer}> <View style={styles.textContainer}>
<AppText style={styles.title}>{title}</AppText> <AppText style={styles.title}>{formattedTitle}</AppText>
{subtitle && <AppText style={styles.subtitle}>{subtitle}</AppText>} {subtitle && <AppText style={styles.subtitle}>{subtitle}</AppText>}
</View> </View>
<TouchableOpacity onPress={() => navigate(true)} hitSlop={HIT_SLOP}> <TouchableOpacity onPress={() => navigate(true)} hitSlop={HIT_SLOP}>
+89 -118
View File
@@ -1,9 +1,10 @@
import React, { Component } from 'react' import React, { useEffect, useState } from 'react'
import { StyleSheet, View } from 'react-native' import { Platform, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Keyboard } from 'react-native' import { Keyboard } from 'react-native'
import DateTimePicker from 'react-native-modal-datetime-picker-nevo' import DateTimePicker from 'react-native-modal-datetime-picker'
import moment from 'moment' import moment from 'moment'
import { useTranslation } from 'react-i18next'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import AppTextInput from '../common/app-text-input' import AppTextInput from '../common/app-text-input'
@@ -11,153 +12,123 @@ import Segment from '../common/segment'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getDate } from '../../slices/date' import { getDate } from '../../slices/date'
import { isTemperatureOutOfRange, isPreviousTemperature } from '../helpers/cycle-day' import {
getTemperatureOutOfRangeMessage,
getPreviousTemperature,
formatTemperature,
} from '../helpers/cycle-day'
import { temperature as labels } from '../../i18n/en/cycle-day' import { temperature as labels } from '../../i18n/en/cycle-day'
import { Colors, Containers, Sizes, Spacing } from '../../styles' import { Colors, Containers, Sizes, Spacing } from '../../styles'
const formatTemperature = value => value === null const Temperature = ({ data, date, save }) => {
? value const { t } = useTranslation()
: Number.parseFloat(value).toFixed(2) const [isTimePickerVisible, setIsTimePickerVisible] = useState(false)
const [temperature, setTemperature] = useState(
formatTemperature(data.value) || getPreviousTemperature(date)
)
class Temperature extends Component { // update state in parent component once to ensure
// that pre-filled values are saved on button click
static propTypes = { useEffect(() => {
data: PropTypes.object, if (temperature) {
date: PropTypes.string.isRequired, save(temperature, 'value')
save: PropTypes.func
}
constructor(props) {
super(props)
const { data, date } = this.props
const { value } = data
const { shouldShowSuggestion, suggestedTemperature } =
isPreviousTemperature(date)
this.state = {
isTimePickerVisible: false,
shouldShowSuggestion,
suggestedTemperature: formatTemperature(suggestedTemperature),
value: formatTemperature(value)
} }
}, [])
function onChangeTemperature(value) {
const formattedValue = value.replace(',', '.').trim()
if (!Number(formattedValue) && value !== '') return false
setTemperature(formattedValue)
} }
onCancelTimePicker = () => { function onShowTimePicker() {
this.setState({ isTimePickerVisible: false })
}
onChangeTemperature = (value) => {
if (!Number(value)) return false
this.setState({
value: value.trim(),
shouldShowSuggestion: false
})
}
onShowTimePicker = () => {
Keyboard.dismiss() Keyboard.dismiss()
this.setState({ isTimePickerVisible: true }) setIsTimePickerVisible(true)
} }
setTemperature = () => { function setTime(jsDate) {
const { value } = this.state
this.props.save(value, 'value')
}
setTime = (jsDate) => {
const time = moment(jsDate).format('HH:mm') const time = moment(jsDate).format('HH:mm')
const isTimePickerVisible = false
this.props.save(time, 'time') save(time, 'time')
this.setState({ isTimePickerVisible }) setIsTimePickerVisible(false)
} }
render() { const { time } = data
const { shouldShowSuggestion, suggestedTemperature, value } = this.state
const { time } = this.props.data
const inputStyle = (shouldShowSuggestion && value === null) const inputStyle = { color: Colors.greyDark }
? { color: Colors.grey } const outOfRangeWarning = getTemperatureOutOfRangeMessage(temperature)
: {color: Colors.greyDark}
const outOfRangeWarning = isTemperatureOutOfRange(value)
let temperatureToShow = null
if (value) { return (
temperatureToShow = value <React.Fragment>
} else if (shouldShowSuggestion) { <Segment>
temperatureToShow = suggestedTemperature <AppText style={styles.title}>{labels.temperature.explainer}</AppText>
} <View style={styles.container}>
return (
<React.Fragment>
<Segment>
<AppText style={styles.title}>{labels.temperature.explainer}</AppText>
<View style={styles.container}>
<AppTextInput
value={temperatureToShow === null ? '' : temperatureToShow}
onChangeText={this.onChangeTemperature}
onEndEditing={this.setTemperature}
keyboardType="numeric"
maxLength={5}
style={inputStyle}
testID="temperatureInput"
underlineColorAndroid="transparent"
/>
<AppText>°C</AppText>
</View>
{ outOfRangeWarning !== null &&
<View style={styles.hintContainer}>
<AppText style={styles.hint}>{outOfRangeWarning}</AppText>
</View>
}
</Segment>
<Segment>
<AppText style={styles.title}>{labels.time}</AppText>
<AppTextInput <AppTextInput
onFocus={this.onShowTimePicker} value={temperature}
testID='timeInput' onChangeText={onChangeTemperature}
value={time} onEndEditing={() => save(temperature, 'value')}
keyboardType="numeric"
maxLength={5}
style={inputStyle}
testID="temperatureInput"
underlineColorAndroid="transparent"
/> />
<DateTimePicker <AppText>°C</AppText>
isVisible={this.state.isTimePickerVisible} </View>
mode="time" {!!outOfRangeWarning && (
onConfirm={this.setTime} <View style={styles.hintContainer}>
onCancel={this.onCancelTimePicker} <AppText style={styles.hint}>{outOfRangeWarning}</AppText>
/> </View>
</Segment> )}
</React.Fragment> </Segment>
) <Segment>
} <AppText style={styles.title}>{labels.time}</AppText>
<AppTextInput
onFocus={onShowTimePicker}
testID="timeInput"
value={time}
/>
<DateTimePicker
isVisible={isTimePickerVisible}
mode="time"
onConfirm={setTime}
onCancel={() => setIsTimePickerVisible(false)}
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
headerTextIOS={t('labels.shared.dateTimePickerTitle')}
/>
</Segment>
</React.Fragment>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
...Containers.rowContainer ...Containers.rowContainer,
}, },
hint: { hint: {
fontStyle: 'italic', fontStyle: 'italic',
fontSize: Sizes.small fontSize: Sizes.small,
}, },
hintContainer: { hintContainer: {
marginVertical: Spacing.tiny marginVertical: Spacing.tiny,
}, },
title: { title: {
fontSize: Sizes.subtitle fontSize: Sizes.subtitle,
} },
}) })
Temperature.propTypes = {
const mapStateToProps = (state) => { data: PropTypes.object.isRequired,
return({ date: PropTypes.string.isRequired,
date: getDate(state), save: PropTypes.func.isRequired,
})
} }
export default connect( const mapStateToProps = (state) => {
mapStateToProps, return {
null, date: getDate(state),
)(Temperature) }
}
export default connect(mapStateToProps, null)(Temperature)
+15 -9
View File
@@ -1,7 +1,14 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Modal, StyleSheet, TouchableOpacity, View } from 'react-native' import {
Modal,
Platform,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import AppIcon from '../common/app-icon' import AppIcon from '../common/app-icon'
import CloseIcon from '../common/close-icon'
import MenuItem from './menu-item' import MenuItem from './menu-item'
import { Colors, Sizes } from '../../styles' import { Colors, Sizes } from '../../styles'
@@ -14,6 +21,7 @@ const settingsMenuItems = [
{ name: menuItems.settings, component: 'SettingsMenu' }, { name: menuItems.settings, component: 'SettingsMenu' },
{ name: menuItems.about, component: 'About' }, { name: menuItems.about, component: 'About' },
{ name: menuItems.license, component: 'License' }, { name: menuItems.license, component: 'License' },
{ name: menuItems.privacyPolicy, component: 'PrivacyPolicy' },
] ]
export default class HamburgerMenu extends Component { export default class HamburgerMenu extends Component {
@@ -34,12 +42,12 @@ export default class HamburgerMenu extends Component {
<React.Fragment> <React.Fragment>
{!shouldShowMenu && ( {!shouldShowMenu && (
<TouchableOpacity onPress={this.toggleMenu} hitSlop={HIT_SLOP}> <TouchableOpacity onPress={this.toggleMenu} hitSlop={HIT_SLOP}>
<AppIcon name='dots-three-vertical' color={Colors.orange} /> <AppIcon name="dots-three-vertical" color={Colors.orange} />
</TouchableOpacity> </TouchableOpacity>
)} )}
{shouldShowMenu && ( {shouldShowMenu && (
<Modal <Modal
animationType='fade' animationType="fade"
onRequestClose={this.toggleMenu} onRequestClose={this.toggleMenu}
transparent={true} transparent={true}
visible={shouldShowMenu} visible={shouldShowMenu}
@@ -49,12 +57,9 @@ export default class HamburgerMenu extends Component {
style={styles.blackBackground} style={styles.blackBackground}
></TouchableOpacity> ></TouchableOpacity>
<View style={styles.menu}> <View style={styles.menu}>
<TouchableOpacity <View style={styles.iconContainer}>
onPress={this.toggleMenu} <CloseIcon color={'black'} onClose={() => this.toggleMenu()} />
style={styles.iconContainer} </View>
>
<AppIcon name='cross' color='black' />
</TouchableOpacity>
{settingsMenuItems.map((item) => ( {settingsMenuItems.map((item) => (
<MenuItem <MenuItem
item={item} item={item}
@@ -85,6 +90,7 @@ const styles = StyleSheet.create({
backgroundColor: 'white', backgroundColor: 'white',
height: '100%', height: '100%',
padding: Sizes.base, padding: Sizes.base,
paddingTop: Platform.OS === 'ios' ? Sizes.huge : Sizes.base,
position: 'absolute', position: 'absolute',
width: '60%', width: '60%',
}, },
+6 -3
View File
@@ -1,4 +1,5 @@
import { LocalDate } from 'js-joda' import { LocalDate } from 'js-joda'
import { verticalScale } from 'react-native-size-matters'
import { Colors, Fonts, Sizes } from '../../styles' import { Colors, Fonts, Sizes } from '../../styles'
@@ -12,7 +13,7 @@ export const toCalFormat = (bleedingDaysSortedByDate) => {
customStyles: { customStyles: {
container: { container: {
backgroundColor: shades[day.bleeding.value], backgroundColor: shades[day.bleeding.value],
paddingTop: 2, paddingTop: verticalScale(2),
}, },
text: { text: {
color: Colors.turquoiseLight, color: Colors.turquoiseLight,
@@ -62,8 +63,9 @@ export const todayToCalFormat = () => {
const styles = { const styles = {
calendarToday: { calendarToday: {
fontFamily: Fonts.bold, fontFamily: 'Jost-Bold',
color: Colors.purple, fontWeight: 'bold',
color: Colors.purple
}, },
} }
@@ -73,6 +75,7 @@ export const calendarTheme = {
monthTextColor: Colors.purple, monthTextColor: Colors.purple,
textDayFontFamily: Fonts.main, textDayFontFamily: Fonts.main,
textMonthFontFamily: Fonts.bold, textMonthFontFamily: Fonts.bold,
textMonthFontWeight: 'bold',
textDayHeaderFontFamily: Fonts.bold, textDayHeaderFontFamily: Fonts.bold,
textDayFontSize: Sizes.small, textDayFontSize: Sizes.small,
textMonthFontSize: Sizes.subtitle, textMonthFontSize: Sizes.subtitle,
+146 -113
View File
@@ -1,6 +1,10 @@
import { ChronoUnit, LocalDate, LocalTime } from 'js-joda' import { ChronoUnit, LocalDate, LocalTime } from 'js-joda'
import { getPreviousTemperature, saveSymptom } from '../../db' import {
getPreviousTemperatureForDate,
saveSymptom,
mapRealmObjToJsObj,
} from '../../db'
import { scaleObservable } from '../../local-storage' import { scaleObservable } from '../../local-storage'
import * as labels from '../../i18n/en/cycle-day' import * as labels from '../../i18n/en/cycle-day'
@@ -23,39 +27,35 @@ const temperatureLabels = labels.temperature
const minutes = ChronoUnit.MINUTES const minutes = ChronoUnit.MINUTES
const isNumber = (value) => typeof value === 'number' const isNumber = (value) => typeof value === 'number'
export const shouldShow = (value) => value !== null ? true : false export const shouldShow = (value) => (value !== null ? true : false)
export const isPreviousTemperature = (temperature) => { export const formatTemperature = (temperature) =>
const previousTemperature = getPreviousTemperature(temperature) !temperature
const shouldShowSuggestion = previousTemperature ? true : false ? temperature
const suggestedTemperature = previousTemperature ? : Number.parseFloat(temperature.toString()).toFixed(2)
previousTemperature.toString() : null
return { shouldShowSuggestion, suggestedTemperature } export const getPreviousTemperature = (date) => {
const previousTemperature = getPreviousTemperatureForDate(date)
return formatTemperature(previousTemperature)
} }
export const isTemperatureOutOfRange = (temperature) => { export const getTemperatureOutOfRangeMessage = (temperature) => {
if (!temperature) return null if (!temperature) return null
const value = Number(temperature) const value = Number(temperature)
const range = { min: TEMP_MIN, max: TEMP_MAX }
const scale = scaleObservable.value const scale = scaleObservable.value
let warningMsg = null return value < TEMP_MIN || value > TEMP_MAX
? labels.temperature.outOfAbsoluteRangeWarning
if (value < range.min || value > range.max) { : value < scale.min || value > scale.max
warningMsg = labels.temperature.outOfAbsoluteRangeWarning ? labels.temperature.outOfRangeWarning
} else if (value < scale.min || value > scale.max) { : ''
warningMsg = labels.temperature.outOfRangeWarning
}
return warningMsg
} }
export const blank = { export const blank = {
bleeding: { bleeding: {
exclude: false, exclude: false,
value: null value: null,
}, },
cervix: { cervix: {
exclude: false, exclude: false,
@@ -64,9 +64,9 @@ export const blank = {
position: null, position: null,
}, },
desire: { desire: {
value: null value: null,
}, },
mood:{ mood: {
happy: null, happy: null,
sad: null, sad: null,
stressed: null, stressed: null,
@@ -77,16 +77,16 @@ export const blank = {
fatigue: null, fatigue: null,
angry: null, angry: null,
other: null, other: null,
note: null note: null,
}, },
mucus: { mucus: {
exclude: false, exclude: false,
feeling: null, feeling: null,
texture: null, texture: null,
value: null value: null,
}, },
note: { note: {
value: null value: null,
}, },
pain: { pain: {
cramps: null, cramps: null,
@@ -97,7 +97,7 @@ export const blank = {
tenderBreasts: null, tenderBreasts: null,
migraine: null, migraine: null,
other: null, other: null,
note: null note: null,
}, },
sex: { sex: {
solo: null, solo: null,
@@ -111,14 +111,14 @@ export const blank = {
diaphragm: null, diaphragm: null,
none: null, none: null,
other: null, other: null,
note: null note: null,
}, },
temperature: { temperature: {
exclude: false, exclude: false,
note: null, note: null,
time: LocalTime.now().truncatedTo(minutes).toString(), time: LocalTime.now().truncatedTo(minutes).toString(),
value: null value: null,
} },
} }
export const symtomPage = { export const symtomPage = {
@@ -126,11 +126,13 @@ export const symtomPage = {
excludeText: labels.bleeding.exclude.explainer, excludeText: labels.bleeding.exclude.explainer,
note: null, note: null,
selectBoxGroups: null, selectBoxGroups: null,
selectTabGroups: [{ selectTabGroups: [
key: 'value', {
options: getLabelsList(bleedingLabels), key: 'value',
title: labels.bleeding.heaviness.explainer, options: getLabelsList(bleedingLabels),
}] title: labels.bleeding.heaviness.explainer,
},
],
}, },
cervix: { cervix: {
excludeText: cervixLabels.excludeExplainer, excludeText: cervixLabels.excludeExplainer,
@@ -151,18 +153,20 @@ export const symtomPage = {
key: 'position', key: 'position',
options: getLabelsList(cervixLabels.position.categories), options: getLabelsList(cervixLabels.position.categories),
title: cervixLabels.position.explainer, title: cervixLabels.position.explainer,
} },
] ],
}, },
desire: { desire: {
excludeText: null, excludeText: null,
note: null, note: null,
selectBoxGroups: null, selectBoxGroups: null,
selectTabGroups: [{ selectTabGroups: [
key: 'value', {
options: getLabelsList(intensityLabels), key: 'value',
title: labels.desire.explainer options: getLabelsList(intensityLabels),
}] title: labels.desire.explainer,
},
],
}, },
mucus: { mucus: {
excludeText: mucusLabels.excludeExplainer, excludeText: mucusLabels.excludeExplainer,
@@ -178,34 +182,38 @@ export const symtomPage = {
key: 'texture', key: 'texture',
options: getLabelsList(mucusLabels.texture.categories), options: getLabelsList(mucusLabels.texture.categories),
title: mucusLabels.texture.explainer, title: mucusLabels.texture.explainer,
} },
] ],
}, },
mood: { mood: {
excludeText: null, excludeText: null,
note: null, note: null,
selectBoxGroups: [{ selectBoxGroups: [
key: 'mood', {
options: moodLabels, key: 'mood',
title: labels.mood.explainer options: moodLabels,
}], title: labels.mood.explainer,
selectTabGroups: null },
],
selectTabGroups: null,
}, },
note: { note: {
excludeText: null, excludeText: null,
note: noteDescription, note: noteDescription,
selectBoxGroups: null, selectBoxGroups: null,
selectTabGroups: null selectTabGroups: null,
}, },
pain: { pain: {
excludeText: null, excludeText: null,
note: null, note: null,
selectBoxGroups: [{ selectBoxGroups: [
key: 'pain', {
options: painLabels, key: 'pain',
title: labels.pain.explainer options: painLabels,
}], title: labels.pain.explainer,
selectTabGroups: null },
],
selectTabGroups: null,
}, },
sex: { sex: {
excludeText: null, excludeText: null,
@@ -220,40 +228,42 @@ export const symtomPage = {
key: 'contraceptives', key: 'contraceptives',
options: contraceptiveLabels, options: contraceptiveLabels,
title: labels.contraceptives.explainer, title: labels.contraceptives.explainer,
} },
], ],
selectTabGroups: null selectTabGroups: null,
}, },
temperature: { temperature: {
excludeText: temperatureLabels.exclude.explainer, excludeText: temperatureLabels.exclude.explainer,
note: temperatureLabels.note.explainer, note: temperatureLabels.note.explainer,
selectBoxGroups: null, selectBoxGroups: null,
selectTabGroups: null selectTabGroups: null,
} },
} }
export const save = { export const save = {
bleeding: (data, date, shouldDeleteData) => { bleeding: (data, date, shouldDeleteData) => {
const { exclude, value } = data const { exclude, value } = data
const isDataEntered = isNumber(value) const isDataEntered = isNumber(value)
const valuesToSave = shouldDeleteData || !isDataEntered const valuesToSave =
? null : { value, exclude } shouldDeleteData || !isDataEntered ? null : { value, exclude }
saveSymptom('bleeding', date, valuesToSave) saveSymptom('bleeding', date, valuesToSave)
}, },
cervix: (data, date, shouldDeleteData) => { cervix: (data, date, shouldDeleteData) => {
const { opening, firmness, position, exclude } = data const { opening, firmness, position, exclude } = data
const isDataEntered = ['opening', 'firmness', 'position'].some( const isDataEntered = ['opening', 'firmness', 'position'].some((value) =>
value => isNumber(data[value])) isNumber(data[value])
const valuesToSave = shouldDeleteData || !isDataEntered )
? null : { opening, firmness, position, exclude } const valuesToSave =
shouldDeleteData || !isDataEntered
? null
: { opening, firmness, position, exclude }
saveSymptom('cervix', date, valuesToSave) saveSymptom('cervix', date, valuesToSave)
}, },
desire: (data, date, shouldDeleteData) => { desire: (data, date, shouldDeleteData) => {
const { value } = data const { value } = data
const valuesToSave = shouldDeleteData || !isNumber(value) const valuesToSave = shouldDeleteData || !isNumber(value) ? null : { value }
? null : { value }
saveSymptom('desire', date, valuesToSave) saveSymptom('desire', date, valuesToSave)
}, },
@@ -262,10 +272,18 @@ export const save = {
}, },
mucus: (data, date, shouldDeleteData) => { mucus: (data, date, shouldDeleteData) => {
const { feeling, texture, exclude } = data const { feeling, texture, exclude } = data
const isDataEntered = ['feeling', 'texture'].some( const isDataEntered = ['feeling', 'texture'].some((value) =>
value => isNumber(data[value])) isNumber(data[value])
const valuesToSave = shouldDeleteData || !isDataEntered )
? null : { feeling, texture, value: computeNfpValue(feeling, texture), exclude } const valuesToSave =
shouldDeleteData || !isDataEntered
? null
: {
feeling,
texture,
value: computeNfpValue(feeling, texture),
exclude,
}
saveSymptom('mucus', date, valuesToSave) saveSymptom('mucus', date, valuesToSave)
}, },
@@ -288,21 +306,20 @@ export const save = {
exclude, exclude,
note, note,
time, time,
value: Number(value) value: Number(value),
} }
saveSymptom( saveSymptom(
'temperature', 'temperature',
date, date,
(shouldDeleteData || value === null) ? null : valuesToSave shouldDeleteData || value === null ? null : valuesToSave
) )
} },
} }
const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => { const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => {
const isDataEntered = Object.keys(data).some(key => data[key] !== null) const isDataEntered = Object.keys(data).some((key) => data[key] !== null)
const valuesToSave = shouldDeleteData || !isDataEntered const valuesToSave = shouldDeleteData || !isDataEntered ? null : data
? null : data
saveSymptom(symptom, date, valuesToSave) saveSymptom(symptom, date, valuesToSave)
} }
@@ -326,46 +343,60 @@ const label = {
return temperatureLabel return temperatureLabel
} }
}, },
mucus: mucus => { mucus: (mucus) => {
const filledCategories = ['feeling', 'texture'].filter(c => isNumber(mucus[c])) const filledCategories = ['feeling', 'texture'].filter((c) =>
let label = filledCategories.map(category => { isNumber(mucus[c])
return labels.mucus.subcategories[category] + ': ' + labels.mucus[category].categories[mucus[category]] )
}).join(', ') let label = filledCategories
.map((category) => {
return (
labels.mucus.subcategories[category] +
': ' +
labels.mucus[category].categories[mucus[category]]
)
})
.join(', ')
if (isNumber(mucus.value)) label += `\n => ${labels.mucusNFP[mucus.value]}` if (isNumber(mucus.value)) label += ` => ${labels.mucusNFP[mucus.value]}`
if (mucus.exclude) label = `(${label})` if (mucus.exclude) label = `(${label})`
return label return label
}, },
cervix: cervix => { cervix: (cervix) => {
const filledCategories = ['opening', 'firmness', 'position'].filter(c => isNumber(cervix[c])) const filledCategories = ['opening', 'firmness', 'position'].filter((c) =>
let label = filledCategories.map(category => { isNumber(cervix[c])
return labels.cervix.subcategories[category] + ': ' + labels.cervix[category].categories[cervix[category]] )
}).join(', ') let label = filledCategories
.map((category) => {
return (
labels.cervix.subcategories[category] +
': ' +
labels.cervix[category].categories[cervix[category]]
)
})
.join(', ')
if (cervix.exclude) label = `(${label})` if (cervix.exclude) label = `(${label})`
return label return label
}, },
note: note => note.value, note: (note) => note.value,
desire: ({ value }) => { desire: ({ value }) => {
if (isNumber(value)) { if (isNumber(value)) {
return intensityLabels[value] return intensityLabels[value]
} }
}, },
sex: sex => { sex: (sex) => {
sex = mapRealmObjToJsObj(sex)
const sexLabel = [] const sexLabel = []
if (sex && Object.values({...sex}).some(val => val)){ if (sex && Object.values({ ...sex }).some((val) => val)) {
Object.keys(sex).forEach(key => { Object.keys(sex).forEach((key) => {
if(sex[key] && key !== 'other' && key !== 'note') { if (sex[key] && key !== 'other' && key !== 'note') {
sexLabel.push( sexLabel.push(sexLabels[key] || contraceptiveLabels[key])
sexLabels[key] ||
contraceptiveLabels[key]
)
} }
if(key === 'other' && sex.other) { if (key === 'other' && sex.other) {
let label = contraceptiveLabels[key] let label = contraceptiveLabels[key]
if(sex.note) { if (sex.note) {
label = `${label} (${sex.note})` label = `${label} (${sex.note})`
} }
sexLabel.push(label) sexLabel.push(label)
@@ -374,16 +405,17 @@ const label = {
return sexLabel.join(', ') return sexLabel.join(', ')
} }
}, },
pain: pain => { pain: (pain) => {
pain = mapRealmObjToJsObj(pain)
const painLabel = [] const painLabel = []
if (pain && Object.values({...pain}).some(val => val)){ if (pain && Object.values({ ...pain }).some((val) => val)) {
Object.keys(pain).forEach(key => { Object.keys(pain).forEach((key) => {
if(pain[key] && key !== 'other' && key !== 'note') { if (pain[key] && key !== 'other' && key !== 'note') {
painLabel.push(painLabels[key]) painLabel.push(painLabels[key])
} }
if(key === 'other' && pain.other) { if (key === 'other' && pain.other) {
let label = painLabels[key] let label = painLabels[key]
if(pain.note) { if (pain.note) {
label = `${label} (${pain.note})` label = `${label} (${pain.note})`
} }
painLabel.push(label) painLabel.push(label)
@@ -392,16 +424,17 @@ const label = {
return painLabel.join(', ') return painLabel.join(', ')
} }
}, },
mood: mood => { mood: (mood) => {
mood = mapRealmObjToJsObj(mood)
const moodLabel = [] const moodLabel = []
if (mood && Object.values({...mood}).some(val => val)){ if (mood && Object.values({ ...mood }).some((val) => val)) {
Object.keys(mood).forEach(key => { Object.keys(mood).forEach((key) => {
if(mood[key] && key !== 'other' && key !== 'note') { if (mood[key] && key !== 'other' && key !== 'note') {
moodLabel.push(moodLabels[key]) moodLabel.push(moodLabels[key])
} }
if(key === 'other' && mood.other) { if (key === 'other' && mood.other) {
let label = moodLabels[key] let label = moodLabels[key]
if(mood.note) { if (mood.note) {
label = `${label} (${mood.note})` label = `${label} (${mood.note})`
} }
moodLabel.push(label) moodLabel.push(label)
@@ -409,7 +442,7 @@ const label = {
}) })
return moodLabel.join(', ') return moodLabel.join(', ')
} }
} },
} }
export const getData = (symptom, symptomData) => { export const getData = (symptom, symptomData) => {
+8 -8
View File
@@ -6,19 +6,19 @@ import { general as labels } from '../../i18n/en/cycle-day'
export default function (date) { export default function (date) {
const today = LocalDate.now() const today = LocalDate.now()
const dateToDisplay = LocalDate.parse(date) const dateToDisplay = LocalDate.parse(date)
return today.equals(dateToDisplay) ? return today.equals(dateToDisplay)
labels.today : ? labels.today
moment(date).format('MMMM Do YYYY') : moment(date).format('MMMM Do YYYY')
} }
export function formatDateForShortText (date) { export function formatDateForShortText(date) {
return moment(date.toString()).format('dddd, MMMM Do') return moment(date.toString()).format('dddd, MMMM Do')
} }
export function dateToTitle(dateString) { export function dateToTitle(dateString) {
const today = LocalDate.now() const today = LocalDate.now()
const dateToDisplay = LocalDate.parse(dateString) const dateToDisplay = LocalDate.parse(dateString)
return today.equals(dateToDisplay) ? return today.equals(dateToDisplay)
labels.today : ? labels.today
moment(dateString).format('dddd, Do MMM YYYY') : moment(dateString).format('ddd DD. MMM YY')
} }
+5
View File
@@ -0,0 +1,5 @@
import Toast from 'react-native-simple-toast'
export const showToast = (text) => Toast.show(
text, Toast.SHORT, ['RCTModalHostViewController', 'UIAlertController']
)
+2 -2
View File
@@ -17,8 +17,8 @@ function getTimes(prediction) {
return { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd } return { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd }
} }
export function determinePredictionText(bleedingPrediction) { export function determinePredictionText(bleedingPrediction, t) {
if (!bleedingPrediction.length) return predictLabels.noPrediction if (!bleedingPrediction.length) return t('labels.bleedingPrediction.noPrediction')
const { const {
todayDate, todayDate,
predictedBleedingStart, predictedBleedingStart,
-180
View File
@@ -1,180 +0,0 @@
import React, { Component } from 'react'
import { ScrollView, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { LocalDate } from 'js-joda'
import { connect } from 'react-redux'
import { navigate } from '../slices/navigation'
import { getDate, setDate } from '../slices/date'
import AppText from './common/app-text'
import Button from './common/button'
import cycleModule from '../lib/cycle'
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
import { determinePredictionText, formatWithOrdinalSuffix } from './helpers/home'
import { Colors, Fonts, Sizes, Spacing } from '../styles'
import { home as labels } from '../i18n/en/labels'
class Home extends Component {
static propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func
}
constructor(props) {
super(props)
const today = LocalDate.now()
this.todayDateString = today.toString()
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
this.cycleDayNumber = getCycleDayNumber(this.todayDateString)
const { status, phase, statusText } =
getFertilityStatusForDay(this.todayDateString)
const prediction = getPredictedMenses()
this.prediction = determinePredictionText(prediction)
this.title = `${today.dayOfMonth()} ${today.month()} ${today.year()}`
if (this.cycleDayNumber) {
this.cycleDayText = formatWithOrdinalSuffix(this.cycleDayNumber)
}
if (phase) {
this.phase = phase
this.phaseText = formatWithOrdinalSuffix(phase)
this.status = status
this.statusText = statusText
}
}
navigateToCycleDayView = () => {
this.props.setDate(this.todayDateString)
this.props.navigate('CycleDay')
}
render() {
const {
cycleDayNumber,
cycleDayText,
phase,
phaseText,
prediction,
status,
statusText,
title
} = this
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
>
<AppText style={styles.title}>{title}</AppText>
{cycleDayNumber &&
<View style={styles.line}>
<AppText style={styles.whiteSubtitle}>{cycleDayText}</AppText>
<AppText style={styles.turquoiseText}>{labels.cycleDay}</AppText>
</View>
}
{phase &&
<View style={styles.line}>
<AppText style={styles.whiteSubtitle}>{phaseText}</AppText>
<AppText style={styles.turquoiseText}>
{labels.cyclePhase}
</AppText>
<AppText style={styles.turquoiseText}>{status}</AppText>
<Asterisk />
</View>
}
<View style={styles.line}>
<AppText style={styles.turquoiseText}>{prediction}</AppText>
</View>
<Button isCTA isSmall={false} onPress={this.navigateToCycleDayView}>
{labels.addData}
</Button>
{phase && (
<View style={styles.asteriskLine}>
<Asterisk />
<AppText linkStyle={styles.whiteText} style={styles.greyText}>
{statusText}
</AppText>
</View>
)}
</ScrollView>
)
}
}
const Asterisk = () => {
return <AppText style={styles.asterisk}>*</AppText>
}
const styles = StyleSheet.create({
asterisk: {
color: Colors.orange,
},
container: {
backgroundColor: Colors.purple,
flex: 1,
},
contentContainer: {
padding: Spacing.base,
paddingTop: 0,
},
line: {
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
asteriskLine: {
flexDirection: 'row',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.small,
},
title: {
color: Colors.purpleLight,
fontFamily: Fonts.bold,
fontSize: Sizes.huge,
marginVertical: Spacing.small,
},
turquoiseText: {
color: Colors.turquoise,
fontSize: Sizes.subtitle,
},
whiteSubtitle: {
color: 'white',
fontSize: Sizes.subtitle,
},
whiteText: {
color: 'white',
},
greyText: {
color: Colors.greyLight,
paddingLeft: Spacing.base,
}
})
const mapStateToProps = (state) => {
return ({
date: getDate(state),
})
}
const mapDispatchToProps = (dispatch) => {
return ({
navigate: (page) => dispatch(navigate(page)),
setDate: (date) => dispatch(setDate(date)),
})
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Home)
+9 -4
View File
@@ -3,8 +3,8 @@ import settingsViews from './settings'
import settingsLabels from '../i18n/en/settings' import settingsLabels from '../i18n/en/settings'
const labels = settingsLabels.menuItems const labels = settingsLabels.menuItems
export const isSettingsView = export const isSettingsView = (page) =>
(page) => Object.keys(settingsViews).includes(page) Object.keys(settingsViews).includes(page)
export const pages = [ export const pages = [
{ {
@@ -70,8 +70,13 @@ export const pages = [
label: 'License', label: 'License',
parent: 'SettingsMenu', parent: 'SettingsMenu',
}, },
{
component: 'PrivacyPolicy',
label: 'PrivacyPolicy',
parent: 'SettingsMenu',
},
{ {
component: 'CycleDay', component: 'CycleDay',
parent: 'Home', parent: 'Home',
} },
] ]
+25 -4
View File
@@ -1,17 +1,32 @@
import React from 'react' import React from 'react'
import { Platform, Linking } from 'react-native'
import AppPage from '../common/app-page' import AppPage from '../common/app-page'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import Segment from '../common/segment' import Segment from '../common/segment'
import Button from '../common/button'
import ButtonRow from '../common/button-row'
import labels from '../../i18n/en/settings' import labels from '../../i18n/en/settings'
import links from '../../i18n/en/links' import links from '../../i18n/en/links'
const AboutSection = () => { const AboutSection = () => {
return ( return (
<AppPage title={labels.aboutSection.title} > <AppPage title={labels.aboutSection.title}>
<Segment> <Segment>
<AppText>{labels.aboutSection.text}</AppText> <AppText>{labels.aboutSection.text}</AppText>
<ButtonRow>
{[links.email, links.gitlab, links.website].map((link) => (
<Button
key={link.url}
isCTA
isSmall
onPress={() => Linking.openURL(link.url)}
>
{link.text}
</Button>
))}
</ButtonRow>
</Segment> </Segment>
<Segment title={labels.philosophy.title}> <Segment title={labels.philosophy.title}>
<AppText>{labels.philosophy.text}</AppText> <AppText>{labels.philosophy.text}</AppText>
@@ -21,9 +36,15 @@ const AboutSection = () => {
</Segment> </Segment>
<Segment title={labels.donate.title}> <Segment title={labels.donate.title}>
<AppText>{labels.donate.note}</AppText> <AppText>{labels.donate.note}</AppText>
</Segment> {Platform.OS !== 'ios' && (
<Segment title={labels.website.title}> <Button
<AppText>{links.website.url}</AppText> isCTA
isSmall
onPress={() => Linking.openURL(links.donate.url)}
>
{links.donate.text}
</Button>
)}
</Segment> </Segment>
<Segment title={labels.version.title} last> <Segment title={labels.version.title} last>
<AppText>{require('../../package.json').version}</AppText> <AppText>{require('../../package.json').version}</AppText>
@@ -1 +1 @@
export const EXPORT_FILE_NAME = 'data.csv' export const EXPORT_FILE_NAME = 'drip-data.csv'
@@ -1,6 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import RNFS from 'react-native-fs' import RNFS from 'react-native-fs'
import { Alert, ToastAndroid } from 'react-native' import { Alert } from 'react-native'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../../common/button' import Button from '../../common/button'
@@ -9,6 +9,7 @@ import ConfirmWithPassword from '../common/confirm-with-password'
import alertError from '../common/alert-error' import alertError from '../common/alert-error'
import { clearDb, isDbEmpty } from '../../../db' import { clearDb, isDbEmpty } from '../../../db'
import { showToast } from '../../helpers/general'
import { hasEncryptionObservable } from '../../../local-storage' import { hasEncryptionObservable } from '../../../local-storage'
import settings from '../../../i18n/en/settings' import settings from '../../../i18n/en/settings'
import { shared as sharedLabels } from '../../../i18n/en/labels' import { shared as sharedLabels } from '../../../i18n/en/labels'
@@ -69,7 +70,7 @@ export default class DeleteData extends Component {
clearDb() clearDb()
} }
await this.deleteExportedFile() await this.deleteExportedFile()
ToastAndroid.show(success.message, ToastAndroid.LONG) showToast(success.message)
} catch (err) { } catch (err) {
alertError(errors.couldNotDeleteFile) alertError(errors.couldNotDeleteFile)
} }
@@ -104,4 +105,4 @@ export default class DeleteData extends Component {
DeleteData.propTypes = { DeleteData.propTypes = {
isDeletingData: PropTypes.bool, isDeletingData: PropTypes.bool,
onStartDeletion: PropTypes.func.isRequired onStartDeletion: PropTypes.func.isRequired
} }
@@ -1,6 +1,6 @@
import Share from 'react-native-share' import Share from 'react-native-share'
import { getCycleDaysSortedByDate } from '../../../db' import { getCycleDaysSortedByDate, mapRealmObjToJsObj } from '../../../db'
import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv' import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv'
import alertError from '../common/alert-error' import alertError from '../common/alert-error'
import settings from '../../../i18n/en/settings' import settings from '../../../i18n/en/settings'
@@ -10,7 +10,7 @@ import RNFS from 'react-native-fs'
export default async function exportData() { export default async function exportData() {
let data let data
const labels = settings.export const labels = settings.export
const cycleDaysByDate = getCycleDaysSortedByDate() const cycleDaysByDate = mapRealmObjToJsObj(getCycleDaysSortedByDate())
if (!cycleDaysByDate.length) return alertError(labels.errors.noData) if (!cycleDaysByDate.length) return alertError(labels.errors.noData)
@@ -33,12 +33,11 @@ export default async function exportData() {
url: `file://${path}`, url: `file://${path}`,
subject: labels.subject, subject: labels.subject,
type: 'text/csv', type: 'text/csv',
showAppsToView: true showAppsToView: true,
failOnCancel: false,
}) })
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return alertError(labels.errors.problemSharing) return alertError(labels.errors.problemSharing)
} }
} }
@@ -1,5 +1,5 @@
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' import DocumentPicker from 'react-native-document-picker'
import rnfs from 'react-native-fs' import rnfs from 'react-native-fs'
import importCsv from '../../../lib/import-export/import-from-csv' import importCsv from '../../../lib/import-export/import-from-csv'
import { shared as sharedLabels } from '../../../i18n/en/labels' import { shared as sharedLabels } from '../../../i18n/en/labels'
@@ -7,35 +7,36 @@ import labels from '../../../i18n/en/settings'
import alertError from '../common/alert-error' import alertError from '../common/alert-error'
export function openImportDialog(onImportData) { export function openImportDialog(onImportData) {
Alert.alert( Alert.alert(labels.import.title, labels.import.message, [
labels.import.title, {
labels.import.message, text: sharedLabels.cancel,
[{ style: 'cancel',
text: sharedLabels.cancel, style: 'cancel', onPress: () => { } onPress: () => {},
}, { },
text: labels.import.deleteOption, {
onPress: () => onImportData(true)
}, {
text: labels.import.replaceOption, text: labels.import.replaceOption,
onPress: () => onImportData(false) onPress: () => onImportData(false),
}] },
) {
text: labels.import.deleteOption,
onPress: () => onImportData(true),
},
])
} }
export async function getFileContent() { export async function getFileContent() {
let fileInfo let fileInfo
try { try {
fileInfo = await new Promise((resolve, reject) => { fileInfo = await DocumentPicker.pick({
DocumentPicker.show({ type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
filetype: [DocumentPickerUtil.allFiles()],
}, (err, res) => {
if (err) return reject(err)
resolve(res)
})
}) })
} catch (err) { } catch (error) {
// because cancelling also triggers an error, we do nothing here if (DocumentPicker.isCancel(error)) {
return // User cancelled the picker, exit any dialogs or menus and move on
return
} else {
importError(error)
}
} }
let fileContent let fileContent
@@ -49,11 +50,10 @@ export async function getFileContent() {
} }
export async function importData(shouldDeleteExistingData, fileContent) { export async function importData(shouldDeleteExistingData, fileContent) {
try { try {
await importCsv(fileContent, shouldDeleteExistingData) await importCsv(fileContent, shouldDeleteExistingData)
Alert.alert(sharedLabels.successTitle, labels.import.success.message) Alert.alert(sharedLabels.successTitle, labels.import.success.message)
} catch(err) { } catch (err) {
importError(err.message) importError(err.message)
} }
} }
@@ -61,4 +61,4 @@ export async function importData(shouldDeleteExistingData, fileContent) {
function importError(msg) { function importError(msg) {
const postFixed = `${msg}\n\n${labels.import.errors.postFix}` const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
alertError(postFixed) alertError(postFixed)
} }
+8 -1
View File
@@ -4,7 +4,14 @@ import DataManagement from './data-management'
import Password from './password' import Password from './password'
import About from './about' import About from './about'
import License from './license' import License from './license'
import PrivacyPolicy from './privacy-policy'
export default { export default {
Reminders, NfpSettings, DataManagement, Password, About, License Reminders,
NfpSettings,
DataManagement,
Password,
About,
License,
PrivacyPolicy,
} }
+6 -4
View File
@@ -1,16 +1,18 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import AppPage from '../common/app-page' import AppPage from '../common/app-page'
import AppText from '../common/app-text' import AppText from '../common/app-text'
import Segment from '../common/segment' import Segment from '../common/segment'
import labels from '../../i18n/en/settings'
const License = () => { const License = () => {
const { t } = useTranslation()
const currentYear = new Date().getFullYear()
return ( return (
<AppPage title={labels.license.title}> <AppPage title={t("settings.license.title")}>
<Segment last> <Segment last>
<AppText>{labels.license.text}</AppText> <AppText>{t("settings.license.text", { currentYear })}</AppText>
</Segment> </Segment>
</AppPage> </AppPage>
) )
+4 -3
View File
@@ -5,7 +5,7 @@ import AppIcon from '../../common/app-icon'
import AppPage from '../../common/app-page' import AppPage from '../../common/app-page'
import AppSwitch from '../../common/app-switch' import AppSwitch from '../../common/app-switch'
import AppText from '../../common/app-text' import AppText from '../../common/app-text'
import TemperatureSlider from './temperature-slider' // import TemperatureSlider from './temperature-slider'
import Segment from '../../common/segment' import Segment from '../../common/segment'
import { useCervixObservable, saveUseCervix } from '../../../local-storage' import { useCervixObservable, saveUseCervix } from '../../../local-storage'
@@ -40,10 +40,11 @@ export default class Settings extends Component {
value={shouldUseCervix} value={shouldUseCervix}
/> />
</Segment> </Segment>
<Segment title={labels.tempScale.segmentTitle}> {/* disabled temporarily, TODO https://gitlab.com/bloodyhealth/drip/-/issues/545 */}
{/* <Segment title={labels.tempScale.segmentTitle}>
<AppText>{labels.tempScale.segmentExplainer}</AppText> <AppText>{labels.tempScale.segmentExplainer}</AppText>
<TemperatureSlider /> <TemperatureSlider />
</Segment> </Segment> */}
<Segment last> <Segment last>
<View style={styles.line}> <View style={styles.line}>
<AppIcon <AppIcon
+12 -4
View File
@@ -1,4 +1,5 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from '../../common/button' import Button from '../../common/button'
@@ -8,6 +9,10 @@ import showBackUpReminder from './show-backup-reminder'
import settings from '../../../i18n/en/settings' import settings from '../../../i18n/en/settings'
export default class CreatePassword extends Component { export default class CreatePassword extends Component {
static propTypes = {
changeEncryptionAndRestart: PropTypes.func,
}
constructor() { constructor() {
super() super()
@@ -23,7 +28,7 @@ export default class CreatePassword extends Component {
showBackUpReminder(this.toggleSettingPassword, () => {}) showBackUpReminder(this.toggleSettingPassword, () => {})
} }
render () { render() {
const { isSettingPassword } = this.state const { isSettingPassword } = this.state
const labels = settings.passwordSettings const labels = settings.passwordSettings
@@ -34,8 +39,11 @@ export default class CreatePassword extends Component {
</Button> </Button>
) )
} else { } else {
return <EnterNewPassword /> return (
<EnterNewPassword
changeEncryptionAndRestart={this.props.changeEncryptionAndRestart}
/>
)
} }
} }
} }
+4 -8
View File
@@ -4,13 +4,13 @@ import PropTypes from 'prop-types'
import Button from '../../common/button' import Button from '../../common/button'
import ConfirmWithPassword from '../common/confirm-with-password' import ConfirmWithPassword from '../common/confirm-with-password'
import { changeEncryptionAndRestartApp } from '../../../db'
import labels from '../../../i18n/en/settings' import labels from '../../../i18n/en/settings'
export default class DeletePassword extends Component { export default class DeletePassword extends Component {
static propTypes = { static propTypes = {
onStartDelete: PropTypes.func, onStartDelete: PropTypes.func,
onCancelDelete: PropTypes.func onCancelDelete: PropTypes.func,
changeEncryptionAndRestart: PropTypes.func,
} }
constructor() { constructor() {
@@ -24,10 +24,6 @@ export default class DeletePassword extends Component {
this.props.onStartDelete() this.props.onStartDelete()
} }
startDeletePassword = async () => {
await changeEncryptionAndRestartApp()
}
cancelConfirmationWithPassword = () => { cancelConfirmationWithPassword = () => {
this.setState({ enteringCurrentPassword: false }) this.setState({ enteringCurrentPassword: false })
this.props.onCancelDelete() this.props.onCancelDelete()
@@ -39,7 +35,7 @@ export default class DeletePassword extends Component {
if (enteringCurrentPassword) { if (enteringCurrentPassword) {
return ( return (
<ConfirmWithPassword <ConfirmWithPassword
onSuccess={this.startDeletePassword} onSuccess={this.props.changeEncryptionAndRestart}
onCancel={this.cancelConfirmationWithPassword} onCancel={this.cancelConfirmationWithPassword}
/> />
) )
@@ -51,4 +47,4 @@ export default class DeletePassword extends Component {
</Button> </Button>
) )
} }
} }
@@ -1,20 +1,23 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import nodejs from 'nodejs-mobile-react-native' import nodejs from 'nodejs-mobile-react-native'
import PropTypes from 'prop-types'
import AppText from '../../common/app-text' import AppText from '../../common/app-text'
import AppTextInput from '../../common/app-text-input' import AppTextInput from '../../common/app-text-input'
import Button from '../../common/button' import Button from '../../common/button'
import { requestHash, changeEncryptionAndRestartApp } from '../../../db' import { requestHash } from '../../../db'
import { Colors, Spacing } from '../../../styles' import { Colors, Spacing } from '../../../styles'
import settings from '../../../i18n/en/settings' import settings from '../../../i18n/en/settings'
const LISTENER_TYPE = 'create-or-change-pw' const LISTENER_TYPE = 'create-or-change-pw'
export default class EnterNewPassword extends Component { export default class EnterNewPassword extends Component {
static propTypes = {
constructor() { changeEncryptionAndRestart: PropTypes.func,
}
constructor(props) {
super() super()
this.state = { this.state = {
password: '', password: '',
@@ -23,13 +26,16 @@ export default class EnterNewPassword extends Component {
} }
nodejs.channel.addListener( nodejs.channel.addListener(
LISTENER_TYPE, LISTENER_TYPE,
changeEncryptionAndRestartApp, props.changeEncryptionAndRestart,
this this
) )
} }
componentWillUnmount() { componentWillUnmount() {
nodejs.channel.removeListener(LISTENER_TYPE, changeEncryptionAndRestartApp) nodejs.channel.removeListener(
LISTENER_TYPE,
this.props.changeEncryptionAndRestart
)
} }
savePassword = () => { savePassword = () => {
@@ -52,15 +58,12 @@ export default class EnterNewPassword extends Component {
this.setState({ passwordConfirmation }) this.setState({ passwordConfirmation })
} }
render () { render() {
const { const { password, passwordConfirmation, shouldShowErrorMessage } =
password, this.state
passwordConfirmation,
shouldShowErrorMessage
} = this.state
const labels = settings.passwordSettings const labels = settings.passwordSettings
const isButtonActive = const isButtonActive =
(password.length > 0) && (passwordConfirmation.length > 0) password.length > 0 && passwordConfirmation.length > 0
return ( return (
<React.Fragment> <React.Fragment>
@@ -80,10 +83,14 @@ export default class EnterNewPassword extends Component {
value={passwordConfirmation} value={passwordConfirmation}
secureTextEntry={true} secureTextEntry={true}
/> />
{shouldShowErrorMessage && {shouldShowErrorMessage && (
<AppText style={styles.error}>{labels.passwordsDontMatch}</AppText> <AppText style={styles.error}>{labels.passwordsDontMatch}</AppText>
} )}
<Button isCTA={isButtonActive} onPress={this.savePassword}> <Button
isCTA={isButtonActive}
disabled={!isButtonActive}
onPress={this.savePassword}
>
{labels.savePassword} {labels.savePassword}
</Button> </Button>
</React.Fragment> </React.Fragment>
@@ -94,6 +101,6 @@ export default class EnterNewPassword extends Component {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
error: { error: {
color: Colors.orange, color: Colors.orange,
marginTop: Spacing.base marginTop: Spacing.base,
} },
}) })
+44 -22
View File
@@ -1,4 +1,10 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { navigate } from '../../../slices/navigation'
import { changeDbEncryption } from '../../../db'
import AppPage from '../../common/app-page' import AppPage from '../../common/app-page'
import AppText from '../../common/app-text' import AppText from '../../common/app-text'
@@ -11,14 +17,18 @@ import DeletePassword from './delete'
import { hasEncryptionObservable } from '../../../local-storage' import { hasEncryptionObservable } from '../../../local-storage'
import labels from '../../../i18n/en/settings' import labels from '../../../i18n/en/settings'
export default class PasswordSetting extends Component { class PasswordSetting extends Component {
static propTypes = {
navigate: PropTypes.func,
restartApp: PropTypes.func,
}
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
isPasswordSet: hasEncryptionObservable.value, isPasswordSet: hasEncryptionObservable.value,
isChangingPassword: false, isChangingPassword: false,
isDeletingPassword: false isDeletingPassword: false,
} }
} }
@@ -38,19 +48,17 @@ export default class PasswordSetting extends Component {
this.setState({ isDeletingPassword: false }) this.setState({ isDeletingPassword: false })
} }
changeEncryptionAndRestart = async (hash) => {
await changeDbEncryption(hash)
await this.props.restartApp()
this.props.navigate('Home')
}
render() { render() {
const { isPasswordSet, isChangingPassword, isDeletingPassword } = this.state
const { const { title, explainerEnabled, explainerDisabled } =
isPasswordSet, labels.passwordSettings
isChangingPassword,
isDeletingPassword,
} = this.state
const {
title,
explainerEnabled,
explainerDisabled
} = labels.passwordSettings
return ( return (
<AppPage> <AppPage>
@@ -59,19 +67,25 @@ export default class PasswordSetting extends Component {
{isPasswordSet ? explainerEnabled : explainerDisabled} {isPasswordSet ? explainerEnabled : explainerDisabled}
</AppText> </AppText>
{!isPasswordSet && <CreatePassword/>} {!isPasswordSet && (
<CreatePassword
{(isPasswordSet && !isDeletingPassword) && ( changeEncryptionAndRestart={this.changeEncryptionAndRestart}
<ChangePassword
onStartChange = {this.onChangingPassword}
onCancelChange = {this.onCancelChangingPassword}
/> />
)} )}
{(isPasswordSet && !isChangingPassword) && ( {isPasswordSet && !isDeletingPassword && (
<ChangePassword
onStartChange={this.onChangingPassword}
onCancelChange={this.onCancelChangingPassword}
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
/>
)}
{isPasswordSet && !isChangingPassword && (
<DeletePassword <DeletePassword
onStartDelete = {this.onDeletingPassword} onStartDelete={this.onDeletingPassword}
onCancelDelete = {this.onCancelDeletingPassword} onCancelDelete={this.onCancelDeletingPassword}
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
/> />
)} )}
</Segment> </Segment>
@@ -79,3 +93,11 @@ export default class PasswordSetting extends Component {
) )
} }
} }
const mapDispatchToProps = (dispatch) => {
return {
navigate: (page) => dispatch(navigate(page)),
}
}
export default connect(null, mapDispatchToProps)(PasswordSetting)
@@ -1,28 +1,32 @@
import { Alert } from 'react-native' import { Alert, Platform } from 'react-native'
import { shared } from '../../../i18n/en/labels' import { shared } from '../../../i18n/en/labels'
import labels from '../../../i18n/en/settings' import labels from '../../../i18n/en/settings'
export default function showBackUpReminder(okHandler, cancelHandler, isDelete) { export default function showBackUpReminder(okHandler, cancelHandler, isDelete) {
let title, message const { title, message } = isDelete
if (isDelete) { ? labels.passwordSettings.deleteBackupReminder
title = labels.passwordSettings.deleteBackupReminderTitle : labels.passwordSettings.backupReminder
message = labels.passwordSettings.deleteBackupReminder
} else { const { backupReminderAppendix } = labels.passwordSettings
title = labels.passwordSettings.backupReminderTitle const appendix =
message = labels.passwordSettings.backupReminder Platform.OS === 'ios'
} ? backupReminderAppendix.ios
: backupReminderAppendix.android
Alert.alert( Alert.alert(
title, title,
message, message + appendix,
[{ [
text: shared.cancel, {
onPress: cancelHandler, text: shared.cancel,
style: 'cancel' onPress: cancelHandler,
}, { style: 'cancel',
text: shared.ok, },
onPress: okHandler {
}], text: shared.ok,
onPress: okHandler,
},
],
{ onDismiss: cancelHandler } { onDismiss: cancelHandler }
) )
} }
+13 -11
View File
@@ -12,7 +12,8 @@ import settings from '../../../i18n/en/settings'
export default class ChangePassword extends Component { export default class ChangePassword extends Component {
static propTypes = { static propTypes = {
onStartChange: PropTypes.func, onStartChange: PropTypes.func,
onCancelChange: PropTypes.func onCancelChange: PropTypes.func,
changeEncryptionAndRestart: PropTypes.func,
} }
constructor() { constructor() {
@@ -21,7 +22,7 @@ export default class ChangePassword extends Component {
this.state = { this.state = {
currentPassword: null, currentPassword: null,
enteringCurrentPassword: false, enteringCurrentPassword: false,
enteringNewPassword: false enteringNewPassword: false,
} }
} }
@@ -41,7 +42,7 @@ export default class ChangePassword extends Component {
this.setState({ this.setState({
currentPassword: null, currentPassword: null,
enteringNewPassword: true, enteringNewPassword: true,
enteringCurrentPassword: false enteringCurrentPassword: false,
}) })
} }
@@ -49,17 +50,14 @@ export default class ChangePassword extends Component {
this.setState({ this.setState({
currentPassword: null, currentPassword: null,
enteringNewPassword: false, enteringNewPassword: false,
enteringCurrentPassword: false enteringCurrentPassword: false,
}) })
this.props.onCancelChange() this.props.onCancelChange()
} }
render() { render() {
const { const { enteringCurrentPassword, enteringNewPassword, currentPassword } =
enteringCurrentPassword, this.state
enteringNewPassword,
currentPassword
} = this.state
const labels = settings.passwordSettings const labels = settings.passwordSettings
const isPasswordSet = currentPassword !== null const isPasswordSet = currentPassword !== null
@@ -73,7 +71,11 @@ export default class ChangePassword extends Component {
} }
if (enteringNewPassword) { if (enteringNewPassword) {
return <EnterNewPassword /> return (
<EnterNewPassword
changeEncryptionAndRestart={this.props.changeEncryptionAndRestart}
/>
)
} }
return ( return (
@@ -86,4 +88,4 @@ export default class ChangePassword extends Component {
</Button> </Button>
) )
} }
} }
+38
View File
@@ -0,0 +1,38 @@
import React from 'react'
import { StyleSheet } from 'react-native'
import { useTranslation } from 'react-i18next'
import AppPage from '../common/app-page'
import AppText from '../common/app-text'
import Segment from '../common/segment'
import { Colors, Sizes } from '../../styles'
const PrivacyPolicy = () => {
const { t } = useTranslation()
const sections = ['intro', 'dataUse', 'permissions', 'transparency']
return (
<AppPage title={t('settings.privacyPolicy.title')}>
{sections.map((sectionItem) => {
return (
<Segment last key={sectionItem.id}>
<AppText style={styles.title}>
{t(`settings.privacyPolicy.${sectionItem}.title`)}
</AppText>
<AppText>{t(`settings.privacyPolicy.${sectionItem}.text`)}</AppText>
</Segment>
)
})}
</AppPage>
)
}
const styles = StyleSheet.create({
title: {
color: Colors.purple,
fontSize: Sizes.subtitle,
},
})
export default PrivacyPolicy
@@ -1,14 +1,20 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import DateTimePicker from 'react-native-modal-datetime-picker-nevo' import { Platform } from 'react-native'
import DateTimePicker from 'react-native-modal-datetime-picker'
import PropTypes from 'prop-types'
import AppSwitch from '../../common/app-switch' import AppSwitch from '../../common/app-switch'
import { saveTempReminder, tempReminderObservable } from '../../../local-storage' import {
saveTempReminder,
tempReminderObservable,
} from '../../../local-storage'
import padWithZeros from '../../helpers/pad-time-with-zeros' import padWithZeros from '../../helpers/pad-time-with-zeros'
import labels from '../../../i18n/en/settings' import labels from '../../../i18n/en/settings'
import { withTranslation } from 'react-i18next'
export default class TemperatureReminder extends Component { class TemperatureReminder extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
@@ -42,9 +48,12 @@ export default class TemperatureReminder extends Component {
render() { render() {
const { isEnabled, isTimePickerVisible, time } = this.state const { isEnabled, isTimePickerVisible, time } = this.state
const { t } = this.props
const tempReminderText = time && isEnabled ? const tempReminderText =
labels.tempReminder.timeSet(time) : labels.tempReminder.noTimeSet time && isEnabled
? labels.tempReminder.timeSet(time)
: labels.tempReminder.noTimeSet
return ( return (
<React.Fragment> <React.Fragment>
@@ -58,8 +67,15 @@ export default class TemperatureReminder extends Component {
mode="time" mode="time"
onConfirm={this.onPickDate} onConfirm={this.onPickDate}
onCancel={this.onPickDateCancel} onCancel={this.onPickDateCancel}
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
headerTextIOS={t('labels.shared.dateTimePickerTitle')}
/> />
</React.Fragment> </React.Fragment>
) )
} }
} }
TemperatureReminder.propTypes = {
t: PropTypes.func.isRequired,
}
export default withTranslation()(TemperatureReminder)
+10 -13
View File
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { Dimensions, ImageBackground, StyleSheet, View } from 'react-native' import { ImageBackground, View } from 'react-native'
import { ScaledSheet } from 'react-native-size-matters'
import AppPage from './common/app-page' import AppPage from './common/app-page'
import AppText from './common/app-text' import AppText from './common/app-text'
@@ -11,10 +12,8 @@ import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length'
import {stats as labels} from '../i18n/en/labels' import {stats as labels} from '../i18n/en/labels'
import { Sizes, Spacing, Typography } from '../styles' import { Sizes, Spacing, Typography } from '../styles'
import { fontRatio } from '../config'
const image = require('../assets/cycle-icon.png') const image = require('../assets/cycle-icon.png')
const screen = Dimensions.get('screen')
const Stats = () => { const Stats = () => {
const cycleLengths = cycleModule().getAllCycleLengths() const cycleLengths = cycleModule().getAllCycleLengths()
@@ -28,8 +27,6 @@ const Stats = () => {
[cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel], [cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel],
[numberOfCycles, labels.basisOfStatsEnd] [numberOfCycles, labels.basisOfStatsEnd]
] ]
const height = screen.height * 0.2
const marginTop = (height / 8 - Sizes.icon / fontRatio) / 4
return ( return (
<AppPage contentContainerStyle={styles.pageContainer}> <AppPage contentContainerStyle={styles.pageContainer}>
@@ -42,12 +39,12 @@ const Stats = () => {
<ImageBackground <ImageBackground
source={image} source={image}
imageStyle={styles.image} imageStyle={styles.image}
style={[styles.imageContainter, { height }]} style={styles.imageContainter}
> >
<AppText <AppText
numberOfLines={1} numberOfLines={1}
ellipsizeMode="clip" ellipsizeMode="clip"
style={[styles.accentPurpleGiant, { marginTop }]} style={styles.accentPurpleGiant}
> >
{cycleData.mean} {cycleData.mean}
</AppText> </AppText>
@@ -73,13 +70,14 @@ const column = {
flexDirection: 'column', flexDirection: 'column',
} }
const styles = StyleSheet.create({ const styles = ScaledSheet.create({
accentOrange: { accentOrange: {
...Typography.accentOrange, ...Typography.accentOrange,
fontSize: Sizes.small, fontSize: Sizes.small,
}, },
accentPurpleGiant: { accentPurpleGiant: {
...Typography.accentPurpleGiant, ...Typography.accentPurpleGiant,
marginTop: Spacing.base * (-2),
}, },
accentPurpleHuge: { accentPurpleHuge: {
...Typography.accentPurpleHuge, ...Typography.accentPurpleHuge,
@@ -89,23 +87,22 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingTop: Spacing.base, paddingTop: Spacing.base
}, },
columnLeft: { columnLeft: {
...column, ...column,
flex: 4, flex: 2,
}, },
columnRight: { columnRight: {
...column, ...column,
flex: 5, flex: 3,
paddingTop: Spacing.small, paddingTop: Spacing.small,
}, },
image: { image: {
resizeMode: 'contain', resizeMode: 'contain',
}, },
imageContainter: { imageContainter: {
paddingTop: Spacing.large * 2, paddingTop: Spacing.large * 2.5,
marginBottom: Spacing.large, marginBottom: Spacing.large,
}, },
pageContainer: { pageContainer: {
+1 -1
View File
@@ -1,4 +1,4 @@
import Home from './home' import Home from './Home'
import Calendar from './calendar' import Calendar from './calendar'
import CycleDay from './cycle-day/cycle-day-overview' import CycleDay from './cycle-day/cycle-day-overview'
import Chart from './chart/chart' import Chart from './chart/chart'
+19 -10
View File
@@ -1,4 +1,5 @@
import { PixelRatio } from 'react-native' import { PixelRatio, StatusBar } from 'react-native'
import { scale, verticalScale } from 'react-native-size-matters'
export const ACTION_DELETE = 'delete' export const ACTION_DELETE = 'delete'
export const ACTION_EXPORT = 'export' export const ACTION_EXPORT = 'export'
@@ -16,16 +17,17 @@ export const SYMPTOMS = [
'note', 'note',
] ]
export const fontRatio = PixelRatio.getFontScale()
export const CHART_COLUMN_WIDTH = 32 export const CHART_COLUMN_WIDTH = 32
export const CHART_COLUMN_MIDDLE = CHART_COLUMN_WIDTH / 2 export const CHART_COLUMN_MIDDLE = CHART_COLUMN_WIDTH / 2
export const CHART_DOT_RADIUS = 6 export const CHART_DOT_RADIUS = scale(6)
export const CHART_GRID_LINE_HORIZONTAL_WIDTH = 0.3 export const CHART_GRID_LINE_HORIZONTAL_WIDTH =
export const CHART_ICON_SIZE = 20 PixelRatio.roundToNearestPixel(0.3)
export const CHART_STROKE_WIDTH = 3 export const CHART_ICON_SIZE = scale(20)
export const CHART_SYMPTOM_HEIGHT_RATIO = 0.08 export const CHART_STROKE_WIDTH = scale(3)
export const CHART_XAXIS_HEIGHT_RATIO = 0.1 export const CHART_SYMPTOM_HEIGHT_RATIO = scale(0.08)
export const CHART_YAXIS_WIDTH = 32 export const CHART_XAXIS_HEIGHT_RATIO = scale(0.1)
export const CHART_YAXIS_WIDTH = scale(32)
export const CHART_TICK_WIDTH = scale(44)
export const TEMP_SCALE_MAX = 37.5 export const TEMP_SCALE_MAX = 37.5
export const TEMP_SCALE_MIN = 35.5 export const TEMP_SCALE_MIN = 35.5
@@ -34,4 +36,11 @@ export const TEMP_MAX = 39
export const TEMP_MIN = 35 export const TEMP_MIN = 35
export const TEMP_SLIDER_STEP = 0.5 export const TEMP_SLIDER_STEP = 0.5
export const HIT_SLOP = { top: 20, bottom: 20, left: 20, right: 20 } export const HIT_SLOP = {
top: verticalScale(20),
bottom: verticalScale(20),
left: scale(20),
right: scale(20)
}
export const STATUSBAR_HEIGHT = StatusBar.currentHeight
+44 -33
View File
@@ -2,7 +2,7 @@ import Realm from 'realm'
import { LocalDate, ChronoUnit } from 'js-joda' import { LocalDate, ChronoUnit } from 'js-joda'
import nodejs from 'nodejs-mobile-react-native' import nodejs from 'nodejs-mobile-react-native'
import fs from 'react-native-fs' import fs from 'react-native-fs'
import restart from 'react-native-restart'
import schemas from './schemas' import schemas from './schemas'
import cycleModule from '../lib/cycle' import cycleModule from '../lib/cycle'
import maybeSetNewCycleStart from '../lib/set-new-cycle-start' import maybeSetNewCycleStart from '../lib/set-new-cycle-start'
@@ -11,7 +11,7 @@ let db
let checkIsMensesStart let checkIsMensesStart
let getMensesDaysRightAfter let getMensesDaysRightAfter
export async function openDb (hash) { export async function openDb(hash) {
const realmConfig = {} const realmConfig = {}
if (hash) { if (hash) {
realmConfig.encryptionKey = hashToInt8Array(hash) realmConfig.encryptionKey = hashToInt8Array(hash)
@@ -22,7 +22,7 @@ export async function openDb (hash) {
let tempConnection let tempConnection
try { try {
tempConnection = await Realm.open(realmConfig) tempConnection = await Realm.open(realmConfig)
} catch(err) { } catch (err) {
const isErrorDecrypting = err.toString().includes('decrypt') const isErrorDecrypting = err.toString().includes('decrypt')
const isErrorMnemonic = err.toString().includes('Invalid mnemonic') const isErrorMnemonic = err.toString().includes('Invalid mnemonic')
// tried to open without password, but is encrypted or incorrect pwd // tried to open without password, but is encrypted or incorrect pwd
@@ -36,20 +36,16 @@ export async function openDb (hash) {
let nextSchemaIndex = Realm.schemaVersion(Realm.defaultPath) let nextSchemaIndex = Realm.schemaVersion(Realm.defaultPath)
tempConnection.close() tempConnection.close()
while (nextSchemaIndex < schemas.length - 1) { while (nextSchemaIndex < schemas.length - 1) {
const tempConfig = Object.assign( const tempConfig = Object.assign(realmConfig, schemas[nextSchemaIndex++])
realmConfig,
schemas[nextSchemaIndex++]
)
const migratedRealm = new Realm(tempConfig) const migratedRealm = new Realm(tempConfig)
migratedRealm.close() migratedRealm.close()
} }
// open the Realm with the latest schema // open the Realm with the latest schema
realmConfig.schema = schemas[schemas.length - 1] realmConfig.schema = schemas[schemas.length - 1]
const connection = await Realm.open(Object.assign( const connection = await Realm.open(
realmConfig, Object.assign(realmConfig, schemas[schemas.length - 1])
schemas[schemas.length - 1] )
))
db = connection db = connection
const cycle = cycleModule() const cycle = cycleModule()
@@ -62,18 +58,33 @@ export function closeDb() {
db.close() db.close()
} }
export function mapRealmObjToJsObj(realmObj) {
return realmObj ? JSON.parse(JSON.stringify(realmObj)) : realmObj
}
export function getBleedingDaysSortedByDate() { export function getBleedingDaysSortedByDate() {
return db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) return db
.objects('CycleDay')
.filtered('bleeding != null')
.sorted('date', true)
} }
export function getTemperatureDaysSortedByDate() { export function getTemperatureDaysSortedByDate() {
return db.objects('CycleDay').filtered('temperature != null').sorted('date', true) return db
.objects('CycleDay')
.filtered('temperature != null')
.sorted('date', true)
} }
export function getCycleDaysSortedByDate() { export function getCycleDaysSortedByDate() {
return db.objects('CycleDay').sorted('date', true) const cycleDays = db.objects('CycleDay').sorted('date', true)
return cycleDays
} }
export function getCycleStartsSortedByDate() { export function getCycleStartsSortedByDate() {
return db.objects('CycleDay').filtered('isCycleStart = true').sorted('date', true) return db
.objects('CycleDay')
.filtered('isCycleStart = true')
.sorted('date', true)
} }
export function saveSymptom(symptom, date, val) { export function saveSymptom(symptom, date, val) {
let cycleDay = getCycleDay(date) let cycleDay = getCycleDay(date)
@@ -83,7 +94,10 @@ export function saveSymptom(symptom, date, val) {
if (symptom === 'bleeding') { if (symptom === 'bleeding') {
const mensesDaysAfter = getMensesDaysRightAfter(cycleDay) const mensesDaysAfter = getMensesDaysRightAfter(cycleDay)
maybeSetNewCycleStart({ maybeSetNewCycleStart({
val, cycleDay, mensesDaysAfter, checkIsMensesStart val,
cycleDay,
mensesDaysAfter,
checkIsMensesStart,
}) })
} else { } else {
cycleDay[symptom] = val cycleDay[symptom] = val
@@ -93,7 +107,7 @@ export function saveSymptom(symptom, date, val) {
export function updateCycleStartsForAllCycleDays() { export function updateCycleStartsForAllCycleDays() {
db.write(() => { db.write(() => {
getBleedingDaysSortedByDate().forEach(day => { getBleedingDaysSortedByDate().forEach((day) => {
if (checkIsMensesStart(day)) { if (checkIsMensesStart(day)) {
day.isCycleStart = true day.isCycleStart = true
} }
@@ -106,7 +120,7 @@ export function createCycleDay(dateString) {
db.write(() => { db.write(() => {
result = db.create('CycleDay', { result = db.create('CycleDay', {
date: dateString, date: dateString,
isCycleStart: false isCycleStart: false,
}) })
}) })
return result return result
@@ -116,9 +130,9 @@ export function getCycleDay(dateString) {
return db.objectForPrimaryKey('CycleDay', dateString) return db.objectForPrimaryKey('CycleDay', dateString)
} }
export function getPreviousTemperature(date) { export function getPreviousTemperatureForDate(date) {
const targetDate = LocalDate.parse(date) const targetDate = LocalDate.parse(date)
const winner = getTemperatureDaysSortedByDate().find(candidate => { const winner = getTemperatureDaysSortedByDate().find((candidate) => {
return LocalDate.parse(candidate.date).isBefore(targetDate) return LocalDate.parse(candidate.date).isBefore(targetDate)
}) })
if (!winner) return null if (!winner) return null
@@ -171,13 +185,16 @@ export function tryToImportWithoutDelete(cycleDays) {
} }
export function requestHash(type, pw) { export function requestHash(type, pw) {
nodejs.channel.post('request-SHA512', JSON.stringify({ nodejs.channel.post(
type: type, 'request-SHA512',
message: pw JSON.stringify({
})) type: type,
message: pw,
})
)
} }
export async function changeEncryptionAndRestartApp(hash) { export async function changeDbEncryption(hash) {
let key let key
if (hash) key = hashToInt8Array(hash) if (hash) key = hashToInt8Array(hash)
const defaultPath = db.path const defaultPath = db.path
@@ -187,19 +204,13 @@ export async function changeEncryptionAndRestartApp(hash) {
const copyPath = dir.join('/') const copyPath = dir.join('/')
const exists = await fs.exists(copyPath) const exists = await fs.exists(copyPath)
if (exists) await fs.unlink(copyPath) if (exists) await fs.unlink(copyPath)
// for some reason, realm complains if we give it a key with value undefined db.writeCopyTo({ path: copyPath, encryptionKey: key })
if (key) {
db.writeCopyTo(copyPath, key)
} else {
db.writeCopyTo(copyPath)
}
db.close() db.close()
await fs.unlink(defaultPath) await fs.unlink(defaultPath)
await fs.moveFile(copyPath, defaultPath) await fs.moveFile(copyPath, defaultPath)
restart.Restart()
} }
export function isDbEmpty () { export function isDbEmpty() {
return db.empty return db.empty
} }
+1
View File
@@ -0,0 +1 @@
{}
+42
View File
@@ -0,0 +1,42 @@
{
"labels": {
"bleedingPrediction": {
"noPrediction": "As soon as you have tracked 3 menstrual cycles, drip. will make predictions for the next ones."
},
"home": {
"cycleDay": " day of your cycle",
"cyclePhase": " cycle phase - ",
"addDataForToday": "add data for today"
},
"shared": {
"cancel": "Cancel",
"dateTimePickerTitle": "Pick a time",
"ok": "OK"
}
},
"settings": {
"license": {
"title": "drip. an open-source cycle tracking app",
"text": "Copyright (C) {{currentYear}} Heart of Code e.V.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details: https://www.gnu.org/licenses/gpl-3.0.html."
},
"privacyPolicy": {
"title": "Privacy Policy",
"intro": {
"title": "Introduction",
"text": "This Privacy Policy sets out how the iOS app 'drip.' uses and protects your personal data that you generate and store within the app."
},
"dataUse": {
"title": "Data use",
"text": "Drip. respects and celebrates your privacy. There is no collection of usage data or personal information, no ads, no spyware. Drip. can store data related to menstrual health locally on your device.\n\nThis includes:\n• settings\n• menstrual cycle tracking data\n\nThe data is used to display statistics and apply fertility awareness rules. This data cannot be accessed by other apps.\n\nIf you wish to delete all your app data you can do so by navigating to Settings, Data and Delete app data. This can also be done by uninstalling the app."
},
"permissions": {
"title": "Permissions",
"text": "For using reminders in drip. you need to allow push notifications. If you don't want to use this feature you simply don't allow notifications for the app."
},
"transparency": {
"title": "Transparency",
"text": "You can read through the source code of drip. to ensure the given information is correct. The source code is like a recipe: It tells you how much and what kind of ingredients you need and how you prepare them to cook a tasty meal or program a funky app.\n\nBuon appetito!"
}
}
}
}
+3 -1
View File
@@ -113,7 +113,9 @@ export const mood = {
} }
export const temperature = { export const temperature = {
outOfRangeWarning: 'This temperature value is out of the current range for the temperature chart. You can change the range in the settings.', // disabled temporarily, TODO https://gitlab.com/bloodyhealth/drip/-/issues/545 */}
// outOfRangeWarning: 'This temperature value is out of the current range for the temperature chart. You can change the range in the settings.',
outOfRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
outOfAbsoluteRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.', outOfAbsoluteRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
saveAnyway: 'Save anyway', saveAnyway: 'Save anyway',
temperature: { temperature: {
+37 -27
View File
@@ -3,19 +3,18 @@ const settingsTitles = labels.menuItems
export const home = { export const home = {
unknown: '?', unknown: '?',
phase: n => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`, phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
cycleDay: ' day of your cycle',
cyclePhase: ' cycle phase - ',
addData: 'add data for today'
} }
export const chart = { export const chart = {
tutorial: 'You can swipe the chart to view more dates.' tutorial: 'You can swipe the chart to view more dates.',
} }
export const shared = { export const shared = {
cancel: 'Cancel', cancel: 'Cancel',
save: 'Save', save: 'Save',
dataSaved: 'Symptom data was saved',
dataDeleted: 'Symptom data was deleted',
errorTitle: 'Error', errorTitle: 'Error',
successTitle: 'Success', successTitle: 'Success',
warning: 'Warning', warning: 'Warning',
@@ -26,12 +25,12 @@ export const shared = {
confirmToProceed: 'Confirm to proceed', confirmToProceed: 'Confirm to proceed',
date: 'Date', date: 'Date',
loading: 'Loading ...', loading: 'Loading ...',
noDataWarning: 'You haven\'t entered any data yet.', noDataWarning: "You haven't entered any data yet.",
noTemperatureWarning: 'You haven\'t entered any temperature data yet.', noTemperatureWarning: "You haven't entered any temperature data yet.",
noDataButtonText: 'Start entering data now', noDataButtonText: 'Start entering data now',
enter: 'Enter', enter: 'Enter',
remove: 'Remove', remove: 'Remove',
learnMore: 'Learn more' learnMore: 'Learn more',
} }
export const headerTitles = { export const headerTitles = {
@@ -46,6 +45,7 @@ export const headerTitles = {
Password: settingsTitles.password.name, Password: settingsTitles.password.name,
About: 'About', About: 'About',
License: 'License', License: 'License',
PrivacyPolicy: 'Privacy Policy',
bleeding: 'Bleeding', bleeding: 'Bleeding',
temperature: 'Temperature', temperature: 'Temperature',
mucus: 'Cervical Mucus', mucus: 'Cervical Mucus',
@@ -54,7 +54,7 @@ export const headerTitles = {
desire: 'Desire', desire: 'Desire',
sex: 'Sex', sex: 'Sex',
pain: 'Pain', pain: 'Pain',
mood: 'Mood' mood: 'Mood',
} }
export const stats = { export const stats = {
@@ -65,49 +65,59 @@ export const stats = {
averageLabel: 'Average cycle', averageLabel: 'Average cycle',
minLabel: `Shortest`, minLabel: `Shortest`,
maxLabel: `Longest`, maxLabel: `Longest`,
stdLabel: `Standard\ndeviation` stdLabel: `Standard\ndeviation`,
} }
export const bleedingPrediction = { export const bleedingPrediction = {
noPrediction: `As soon as you have tracked 3 menstrual cycles, drip will make predictions for the next ones.`, predictionInFuture: (startDays, endDays) =>
predictionInFuture: (startDays, endDays) => `Your next period is likely to start in ${startDays} to ${endDays} days.`, `Your next period is likely to start in ${startDays} to ${endDays} days.`,
predictionStartedXDaysLeft: (numberOfDays) => `Your period is likely to start today or within the next ${numberOfDays} days.`, predictionStartedXDaysLeft: (numberOfDays) =>
predictionStarted1DayLeft: 'Your period is likely to start today or tomorrow.', `Your period is likely to start today or within the next ${numberOfDays} days.`,
predictionStarted1DayLeft:
'Your period is likely to start today or tomorrow.',
predictionStartedNoDaysLeft: 'Your period is likely to start today.', predictionStartedNoDaysLeft: 'Your period is likely to start today.',
predictionInPast: (startDate, endDate) => `Based on your documented data, your period was likely to start between ${startDate} and ${endDate}.` predictionInPast: (startDate, endDate) =>
`Based on your documented data, your period was likely to start between ${startDate} and ${endDate}.`,
} }
export const passwordPrompt = { export const passwordPrompt = {
title: 'Unlock app', title: 'Unlock app',
enterPassword: 'Enter password here', enterPassword: 'Enter password here',
deleteDatabaseExplainer: "If you've forgotten your password, unfortunately, there is nothing we can do to recover your data, because it is encrypted with the password only you know. You can, however, delete all your encrypted data and start fresh. Once all data has been erased, you can set a new password in the settings, if you like.", deleteDatabaseExplainer:
"If you've forgotten your password, unfortunately, there is nothing we can do to recover your data, because it is encrypted with the password only you know. You can, however, delete all your encrypted data and start fresh. Once all data has been erased, you can set a new password in the settings, if you like.",
forgotPassword: 'Forgot your password?', forgotPassword: 'Forgot your password?',
deleteDatabaseTitle: 'Forgot your password?', deleteDatabaseTitle: 'Forgot your password?',
deleteData: 'Yes, delete all my data', deleteData: 'Yes, delete all my data',
areYouSureTitle: 'Are you sure?', areYouSureTitle: 'Are you sure?',
areYouSure: 'Are you absolutely sure you want to permanently delete all your data?', areYouSure:
reallyDeleteData: 'Yes, I am sure' 'Are you absolutely sure you want to permanently delete all your data?',
reallyDeleteData: 'Yes, I am sure',
} }
export const fertilityStatus = { export const fertilityStatus = {
fertile: 'fertile', fertile: 'fertile',
infertile: 'infertile', infertile: 'infertile',
fertileUntilEvening: 'Fertile phase ends in the evening', fertileUntilEvening: 'Fertile phase ends in the evening',
unknown: "We cannot show any cycle information because no period data has been added.", unknown:
preOvuText: "With NFP rules, you may assume 5 days of infertility at the beginning of your cycle, provided you don't observe any fertile cervical mucus or cervix values.", 'We cannot show any cycle information because no period data has been added.',
periOvuText: "We were not able to detect both a temperature shift and cervical mucus or cervix shift.", preOvuText:
periOvuUntilEveningText: tempRule => { "With NFP rules, you may assume 5 days of infertility at the beginning of your cycle, provided you don't observe any fertile cervical mucus or cervix values.",
periOvuText:
'We were not able to detect both a temperature shift and cervical mucus or cervix shift.',
periOvuUntilEveningText: (tempRule) => {
return ( return (
'We detected a temperature shift (' + ['regular', '1st exception', '2nd exception'][tempRule] + 'We detected a temperature shift (' +
['regular', '1st exception', '2nd exception'][tempRule] +
' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. In the evening today you may assume infertility, but ' + ' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. In the evening today you may assume infertility, but ' +
'always remember to double-check for yourself. Make sure the data makes sense to you.' 'always remember to double-check for yourself. Make sure the data makes sense to you.'
) )
}, },
postOvuText: tempRule => { postOvuText: (tempRule) => {
return ( return (
'We detected a temperature shift (' + ['regular', '1st exception', '2nd exception'][tempRule] + 'We detected a temperature shift (' +
['regular', '1st exception', '2nd exception'][tempRule] +
' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. You may assume infertility, but always remember to ' + ' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. You may assume infertility, but always remember to ' +
'double-check for yourself. Make sure the data makes sense to you.' 'double-check for yourself. Make sure the data makes sense to you.'
) )
} },
} }
+7 -6
View File
@@ -1,22 +1,23 @@
export default { export default {
gitlab: { gitlab: {
url: 'https://gitlab.com/bloodyhealth/drip', url: 'https://gitlab.com/bloodyhealth/drip',
text: 'GitLab' text: 'GitLab',
}, },
email: { email: {
url: 'mailto:bloodyhealth@mailbox.org', url: 'mailto:drip@mailbox.org',
text: 'email' text: 'email',
}, },
wiki: { wiki: {
url: 'https://gitlab.com/bloodyhealth/drip/wikis/home', url: 'https://gitlab.com/bloodyhealth/drip/wikis/home',
text: 'our wiki' text: 'our wiki',
}, },
website: { website: {
url: 'https://bloodyhealth.gitlab.io/' url: 'https://dripapp.org/',
text: 'Website',
}, },
donate: { donate: {
url: 'https://ko-fi.com/dripapp', url: 'https://ko-fi.com/dripapp',
text: 'here' text: 'Donate here',
}, },
smashicons: { smashicons: {
url: 'https://smashicons.com/', url: 'https://smashicons.com/',
+70 -53
View File
@@ -1,40 +1,40 @@
import links from './links' import links from './links'
const currentYear = new Date().getFullYear()
export default { export default {
title: 'Settings', title: 'Settings',
menuItems: { menuItems: {
reminders: { reminders: {
name: 'Reminders', name: 'Reminders',
text: 'turn on/off reminders' text: 'turn on/off reminders',
}, },
nfpSettings: { nfpSettings: {
name:'NFP settings', name: 'NFP settings',
text: 'define how you want to use NFP', text: 'define how you want to use NFP',
}, },
dataManagement: { dataManagement: {
name: 'Data', name: 'Data',
text: 'import, export or delete your data' text: 'import, export or delete your data',
}, },
password: { password: {
name:'Password', name: 'Password',
text: '' text: '',
}, },
about: 'About', about: 'About',
license: 'License', license: 'License',
settings: 'Settings' settings: 'Settings',
privacyPolicy: 'Privacy Policy',
}, },
export: { export: {
errors: { errors: {
noData: 'There is no data to export', noData: 'There is no data to export',
couldNotConvert: 'Could not convert data to CSV', couldNotConvert: 'Could not convert data to CSV',
problemSharing: 'There was a problem sharing the data export file' problemSharing: 'There was a problem sharing the data export file',
}, },
title: 'My drip data export', title: 'My drip. data export',
subject: 'My drip data export', subject: 'My drip. data export',
button: 'Export data', button: 'Export data',
segmentExplainer: 'Export data in CSV format for backup or so you can use it elsewhere' segmentExplainer:
'Export data in CSV format for backup or so you can use it elsewhere',
}, },
import: { import: {
button: 'Import data', button: 'Import data',
@@ -47,101 +47,118 @@ export default {
errors: { errors: {
couldNotOpenFile: 'Could not open file', couldNotOpenFile: 'Could not open file',
postFix: 'No data was imported or changed', postFix: 'No data was imported or changed',
futureEdit: 'Future dates may only contain a note, no other symptoms' futureEdit: 'Future dates may only contain a note, no other symptoms',
}, },
success: { success: {
message: 'Data successfully imported' message: 'Data successfully imported',
}, },
segmentExplainer: 'Import data in CSV format' segmentExplainer: 'Import data in CSV format',
}, },
deleteSegment: { deleteSegment: {
title: 'Delete app data', title: 'Delete app data',
explainer: 'Delete app data from this phone', explainer: 'Delete app data from this phone',
question: 'Do you want to delete app data from this phone?', question: 'Do you want to delete app data from this phone?',
message: 'Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.', message:
'Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.',
confirmation: 'Delete app data permanently', confirmation: 'Delete app data permanently',
errors: { errors: {
couldNotDeleteFile: 'Could not delete data', couldNotDeleteFile: 'Could not delete data',
postFix: 'No data was deleted or changed', postFix: 'No data was deleted or changed',
noData: 'There is no data to delete' noData: 'There is no data to delete',
}, },
success: { success: {
message: 'App data successfully deleted' message: 'App data successfully deleted',
} },
}, },
tempScale: { tempScale: {
segmentTitle: 'Temperature scale', segmentTitle: 'Temperature scale',
segmentExplainer: 'Change the minimum and maximum value for the temperature chart', segmentExplainer:
'Change the minimum and maximum value for the temperature chart',
min: 'Min', min: 'Min',
max: 'Max', max: 'Max',
loadError: 'Could not load saved temperature scale settings', loadError: 'Could not load saved temperature scale settings',
saveError: 'Could not save temperature scale settings' saveError: 'Could not save temperature scale settings',
}, },
tempReminder: { tempReminder: {
title: 'Temperature reminder', title: 'Temperature reminder',
noTimeSet: 'Set a time for a daily reminder to take your temperature', noTimeSet: 'Set a time for a daily reminder to take your temperature',
timeSet: time => `Daily reminder set for ${time}`, timeSet: (time) => `Daily reminder set for ${time}`,
notification: 'Record your morning temperature' notification: 'Record your morning temperature',
}, },
periodReminder: { periodReminder: {
title: 'Next period reminder', title: 'Next period reminder',
reminderText: 'Get a notification 3 days before your next period is likely to start.', reminderText:
notification: daysToEndOfPrediction => `Your next period is likely to start in 3 to ${daysToEndOfPrediction} days.` 'Get a notification 3 days before your next period is likely to start.',
notification: (daysToEndOfPrediction) =>
`Your next period is likely to start in 3 to ${daysToEndOfPrediction} days.`,
}, },
useCervix: { useCervix: {
title: 'Secondary symptom', title: 'Secondary symptom',
cervixModeOn: 'Cervix values are being used for symptothermal fertility detection. You can switch here to use cervical mucus values for symptothermal fertility detection', cervixModeOn:
cervixModeOff: 'By default, cervical mucus values are being used for symptothermal fertility detection. You can switch here to use cervix values for symptothermal fertility detection' 'Cervix values are being used for symptothermal fertility detection. You can switch here to use cervical mucus values for symptothermal fertility detection',
cervixModeOff:
'By default, cervical mucus values are being used for symptothermal fertility detection. You can switch here to use cervix values for symptothermal fertility detection',
}, },
passwordSettings: { passwordSettings: {
title: 'App password', title: 'App password',
explainerDisabled: "Encrypt the app's database with a password. You need to enter the password every time the app is started.", explainerDisabled:
explainerEnabled: "Password protection and database encryption is currently enabled", "Encrypt the app's database with a password. You need to enter the password every time the app is started.",
explainerEnabled:
'Password protection and database encryption is currently enabled',
setPassword: 'Set password', setPassword: 'Set password',
savePassword: 'Save password', savePassword: 'Save password',
changePassword: 'Change password', changePassword: 'Change password',
deletePassword: 'Delete password', deletePassword: 'Delete password',
enterCurrent: "Please enter your current password", enterCurrent: 'Please enter your current password',
enterNew: "Please enter a new password", enterNew: 'Please enter a new password',
confirmPassword: "Please confirm your password", confirmPassword: 'Please confirm your password',
passwordsDontMatch: "Password and confirmation don't match", passwordsDontMatch: "Password and confirmation don't match",
backupReminderTitle: 'Read this before making changes to your password', backupReminder: {
backupReminder: 'Just to be safe, please backup your data using the export function before making changes to your password.\n\nLonger passwords are better! Consider using a passphrase.\n\nPlease also make sure you do not lose your password. There is no way to recover your data if you do.\n\nMaking any changes to your password setting will keep your data as it was before and restart the app.', title: 'Read this before making changes to your password',
deleteBackupReminderTitle: 'Read this before deleting your password', message: `
deleteBackupReminder: 'Deleting your password means your data will no longer be encrypted.\n\nJust to be safe, please backup your data using the export function before deleting your password.\n\nMaking any changes to your password setting will keep your data as it was before and restart the app.', Just to be safe, please backup your data using the export function before making any changes to your password.\n
Longer passwords are better! Consider using a passphrase.\n
Please also make sure you do not lose your password. There is no way to recover your data if you do.\n
Making any changes to your password setting will keep your data as it was before.\n`,
},
deleteBackupReminder: {
title: 'Read this before deleting your password',
message: `
Deleting your password means your data will no longer be encrypted.\n
Just to be safe, please backup your data using the export function before deleting your password.\n
Making any changes to your password setting will keep your data as it was before and restart the app.\n
`,
},
backupReminderAppendix: {
android:
'After the password is updated the app will automatically restart.',
ios: 'After the password is updated the app will automatically close. Please reopen it manually.',
},
}, },
aboutSection: { aboutSection: {
title: 'About', title: 'About',
text: `Please note that your data is stored locally on your phone and not on a server. This means your data cannot be read by anyone else unless they have access to your phone. We want to ensure that you stay in control of your own data. If you are planning to switch or reset your phone, please remember to export your data before doing so. You can reinstall the app afterwards and import your data.\n\nIf you encounter any technical issues, don't hesitate to contact us via ${links.email.url}. You can also contribute to the code base on ${links.gitlab.url}`, text: `Please note that your data is stored locally on your phone and not on a server. This means your data cannot be read by anyone else unless they have access to your phone. We want to ensure that you stay in control of your own data. If you are planning to switch or reset your phone, please remember to export your data before doing so. You can reinstall the app afterwards and import your data.\n\nIf you encounter any technical issues, don't hesitate to contact us via email. You can also contribute to the code base on Gitlab and visit our website.`,
}, },
philosophy: { philosophy: {
title: 'Remember to think for yourself', title: 'Remember to think for yourself',
text: `drip makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip wants to support you and make period tracking easier, more transparent and secure.`, text: `drip. makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip. wants to support you and make period tracking easier, more transparent and secure.`,
},
license: {
title: 'drip is an open-source cycle tracking app',
text: `Copyright (C) ${currentYear} Bloody Health GbR
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details: https://www.gnu.org/licenses/gpl-3.0.html
You can contact us by bloodyhealth@mailbox.org.`
}, },
version: { version: {
title: 'Version' title: 'Version',
}, },
website: { website: {
title: 'Website' title: 'Website',
}, },
preOvu: { preOvu: {
title: 'Infertile days at cycle start', title: 'Infertile days at cycle start',
note: `drip applies NFP's rules for calculating infertile days at the start of the cycle (see ${links.wiki.url} for more info). However, drip does not currently apply the so called 20-day-rule, which determines infertile days at the cycle start from past cycle lengths in case no past symptothermal info is available.` note: `drip. applies NFP's rules for calculating infertile days at the start of the cycle (see ${links.wiki.url} for more info). However, drip. does not currently apply the so called 20-day-rule, which determines infertile days at the cycle start from past cycle lengths in case no past symptothermal info is available.`,
}, },
credits: { credits: {
title: 'Credits', title: 'Credits',
note: `We love the drip team. Thanks and lots of <3 to all of our condriputors. Thanks to Paula Härtel for the symptom tracking icons. All the other icons are made by ${links.smashicons.url}, ${links.pause08.url}, ${links.kazachek.url} & ${links.freepik.url} from ${links.flaticon.url}.` note: `We love the drip. team. Thanks and lots of <3 to all of our condriputors. Thanks to Paula Härtel for the symptom tracking icons. All the other icons are made by ${links.smashicons.url}, ${links.pause08.url}, ${links.kazachek.url} & ${links.freepik.url} from ${links.flaticon.url}.`,
}, },
donate: { donate: {
title: 'Buy us a coffee!', title: 'Support us',
note: `The Bloody Health team is always grateful for donations, big or small, that help us maintain this app and develop new features. You can donate ${links.donate.url}. Thank you! You're awesome.` note: `The drips are developing this app on a volunteer basis. We are always grateful for support. This could mean condriputing to the code, giving feedback, suggesting improvements or features, testing or donating. It helps and motivates us maintaining this app and developing new features. Thank you for your support!`,
} },
} }
+22 -22
View File
@@ -11,10 +11,10 @@ export const generalInfo = {
3. and menstrual bleeding 3. and menstrual bleeding
the app helps you identify in which phase of the menstrual cycle you are. the app helps you identify in which phase of the menstrual cycle you are.
drip makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip wants to support you and make period tracking easier, more transparent and secure. drip. makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip. wants to support you and make period tracking easier, more transparent and secure.
Please find more info on the sympto-thermal method in ${links.wiki.url}.`, Please find more info on the sympto-thermal method in ${links.wiki.url}.`,
noNfpSymptom: `The app allows you to track this symptom for your information, it is not taken into account for any calculation. On the chart you can check how often you track this symptom.` noNfpSymptom: `The app allows you to track this symptom for your information, it is not taken into account for any calculation. On the chart you can check how often you track this symptom.`,
} }
export default { export default {
@@ -22,7 +22,7 @@ export default {
title: `Tracking menstrual bleeding`, title: `Tracking menstrual bleeding`,
text: `Tracking menstrual bleeding allows you to know the beginning and the end of a menstrual cycle. text: `Tracking menstrual bleeding allows you to know the beginning and the end of a menstrual cycle.
After tracking at least 3 menstrual cycles, drip will give you an overview of After tracking at least 3 menstrual cycles, drip. will give you an overview of
· how long your cycles last on average (in "stats"), · how long your cycles last on average (in "stats"),
· whether the length of your cycles varied significantly (in "stats" and in bleeding predictions) · whether the length of your cycles varied significantly (in "stats" and in bleeding predictions)
· and predict your next 3 cycles with a range of 3 or 5 days (on home screen and "calendar"). · and predict your next 3 cycles with a range of 3 or 5 days (on home screen and "calendar").
@@ -33,7 +33,7 @@ Excluding bleeding values is for tracking bleeding when it's not marking the sta
${generalInfo.nfpTfyReminder}`, ${generalInfo.nfpTfyReminder}`,
}, },
cervix: { cervix: {
title: `Tracking your cervix`, title: `Tracking your cervix`,
text: `The cervix is located inside of the body at the end of the vaginal canal, between the vagina and the uterus. text: `The cervix is located inside of the body at the end of the vaginal canal, between the vagina and the uterus.
@@ -42,15 +42,15 @@ Tracking how open or closed and how firm or soft the cervix feels can help deter
By default, the secondary symptom the app uses for NFP evaluation is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings". By default, the secondary symptom the app uses for NFP evaluation is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings".
· How to identify a fertile cervix? · How to identify a fertile cervix?
A fertile cervix is open and feels soft like your earlobes. In contrast, an infertile cervix feels closed and hard, like the tip of your nose. If the cervix feels anything other than closed and hard, drip takes it as a sign of fertility. On the chart, a fertile cervix is colored in dark yellow, and infertile cervix is colored in light yellow. A fertile cervix is open and feels soft like your earlobes. In contrast, an infertile cervix feels closed and hard, like the tip of your nose. If the cervix feels anything other than closed and hard, drip. takes it as a sign of fertility. On the chart, a fertile cervix is colored in dark yellow, and infertile cervix is colored in light yellow.
${generalInfo.chartNfp} ${generalInfo.chartNfp}
${generalInfo.excludeExplainer} ${generalInfo.excludeExplainer}
${generalInfo.nfpTfyReminder}` ${generalInfo.nfpTfyReminder}`,
}, },
desire: { desire: {
title: 'Tracking sexual desire', title: 'Tracking sexual desire',
text: `The app allows you to track sexual desire independently from sexual activity. text: `The app allows you to track sexual desire independently from sexual activity.
@@ -58,9 +58,9 @@ ${generalInfo.cycleRelation}
${generalInfo.noNfpSymptom} ${generalInfo.noNfpSymptom}
${generalInfo.curiousNfp}` ${generalInfo.curiousNfp}`,
}, },
mood: { mood: {
title: 'Tracking mood', title: 'Tracking mood',
text: `The app allows you to track your mood. text: `The app allows you to track your mood.
@@ -68,16 +68,16 @@ ${generalInfo.cycleRelation}
${generalInfo.noNfpSymptom} ${generalInfo.noNfpSymptom}
${generalInfo.curiousNfp}` ${generalInfo.curiousNfp}`,
}, },
mucus: { mucus: {
title: 'Tracking cervical mucus', title: 'Tracking cervical mucus',
text: `Cervical mucus can help determine in which phase of the menstrual cycle you are. text: `Cervical mucus can help determine in which phase of the menstrual cycle you are.
By default the secondary symptom the app uses for NFP evaluation is cervical mucus. By default the secondary symptom the app uses for NFP evaluation is cervical mucus.
· How to identify fertile cervical mucus? · How to identify fertile cervical mucus?
Tracking the feeling and the texture of your cervical mucus on a daily basis helps you identify changes of the quality of the cervical mucus. The values you enter for both feeling and texture of your cervical mucus are combined by drip into one of five NFP-conforming values. Tracking the feeling and the texture of your cervical mucus on a daily basis helps you identify changes of the quality of the cervical mucus. The values you enter for both feeling and texture of your cervical mucus are combined by drip. into one of five NFP-conforming values.
From lowest to best quality: From lowest to best quality:
· t = (dry feeling + no texture), · t = (dry feeling + no texture),
· ∅ = (no feeling + no texture), · ∅ = (no feeling + no texture),
@@ -87,23 +87,23 @@ From lowest to best quality:
On the chart, cervical mucus is colored in blue: the darker the shade of blue the better the quality of your cervical mucus. On the chart, cervical mucus is colored in blue: the darker the shade of blue the better the quality of your cervical mucus.
Please note that drip does not yet support "parenthesis values": According to NFP rules, you can qualify a cervical mucus value by putting parentheses around it, to indicate that it doesn't fully meet the descriptors of one of the five categories, and instead is in between. This functionality will be supported in the future. Please note that drip. does not yet support "parenthesis values": According to NFP rules, you can qualify a cervical mucus value by putting parentheses around it, to indicate that it doesn't fully meet the descriptors of one of the five categories, and instead is in between. This functionality will be supported in the future.
${generalInfo.chartNfp} ${generalInfo.chartNfp}
${generalInfo.excludeExplainer} ${generalInfo.excludeExplainer}
${generalInfo.nfpTfyReminder}` ${generalInfo.nfpTfyReminder}`,
}, },
note: { note: {
title: 'Notes', title: 'Notes',
text: `Note allows you to track any extra information you want to save. It is the only category that can store information for a date in the future. This can be helpful for reminding you of an appointment. text: `Note allows you to track any extra information you want to save. It is the only category that can store information for a date in the future. This can be helpful for reminding you of an appointment.
${generalInfo.noNfpSymptom} ${generalInfo.noNfpSymptom}
${generalInfo.curiousNfp}` ${generalInfo.curiousNfp}`,
}, },
pain: { pain: {
title: 'Tracking pain', title: 'Tracking pain',
text: `The app allows you to keep track of different kinds of pain you experience. text: `The app allows you to keep track of different kinds of pain you experience.
@@ -111,17 +111,17 @@ ${generalInfo.cycleRelation}
${generalInfo.noNfpSymptom} ${generalInfo.noNfpSymptom}
${generalInfo.curiousNfp}` ${generalInfo.curiousNfp}`,
}, },
sex: { sex: {
title: 'Tracking sex and contraceptives', title: 'Tracking sex and contraceptives',
text: `The app allows you to track sex independently from sexual desire. You can differentiate between masturbation and sex with a partner/partners. Here you can also track your contraceptive method(s). Only sexual activity will be shown in the "chart" section, lighter purple indicating solo sex and darker purple partner sex. Did you know that having an orgasm can help release cramps? text: `The app allows you to track sex independently from sexual desire. You can differentiate between masturbation and sex with a partner/partners. Here you can also track your contraceptive method(s). Only sexual activity will be shown in the "chart" section, lighter purple indicating solo sex and darker purple partner sex. Did you know that having an orgasm can help release cramps?
${generalInfo.noNfpSymptom} ${generalInfo.noNfpSymptom}
${generalInfo.curiousNfp}` ${generalInfo.curiousNfp}`,
}, },
temperature: { temperature: {
title: 'Tracking body basal temperature', title: 'Tracking body basal temperature',
text: `One of the body signs you need to track for knowing your fertility status is your body basal temperature. The body temperature changes over the course of a menstrual cycle, it rises after ovulation. text: `One of the body signs you need to track for knowing your fertility status is your body basal temperature. The body temperature changes over the course of a menstrual cycle, it rises after ovulation.
@@ -140,6 +140,6 @@ ${generalInfo.chartNfp}
${generalInfo.excludeExplainer} ${generalInfo.excludeExplainer}
${generalInfo.nfpTfyReminder}` ${generalInfo.nfpTfyReminder}`,
}, },
} }
+28
View File
@@ -0,0 +1,28 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// translation files
import en from './en.json';
const resources = {
en: { translation: en },
};
i18n
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
resources,
fallbackLng: 'en',
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
}
});
export default i18n;
+1
View File
@@ -1,4 +1,5 @@
import { AppRegistry } from 'react-native' import { AppRegistry } from 'react-native'
import AppWrapper from './components/app-wrapper' import AppWrapper from './components/app-wrapper'
import './i18n/i18n';
AppRegistry.registerComponent('drip', () => AppWrapper) AppRegistry.registerComponent('drip', () => AppWrapper)
+67
View File
@@ -0,0 +1,67 @@
platform :ios, '10.0'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
target 'drip' do
# Pods for drip
pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
pod 'React', :path => '../node_modules/react-native/'
pod 'React-Core', :path => '../node_modules/react-native/'
pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
pod 'React-ART', :path => '../node_modules/react-native/Libraries/ART'
pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon"
pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
target 'dripTests' do
inherit! :search_paths
# Pods for testing
end
use_native_modules!
use_frameworks!
# This is fix to make ios build see images, should be removed after upgrade to rn 0.63.2
# https://stackoverflow.com/questions/63949851/react-native-ios-not-showing-images-pods-issue
post_install do |installer|
find_and_replace("../node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m",
"_currentFrame.CGImage;","_currentFrame.CGImage ;} else { [super displayLayer:layer];")
find_and_replace("../node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm",
"_initializeModules:(NSArray<id<RCTBridgeModule>> *)modules", "_initializeModules:(NSArray<Class> *)modules")
find_and_replace("../node_modules/react-native/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm",
"RCTBridgeModuleNameForClass(module))", "RCTBridgeModuleNameForClass(Class(module)))")
end
def find_and_replace(dir, findstr, replacestr)
Dir[dir].each do |name|
text = File.read(name)
replace = text.gsub(findstr,replacestr)
if text != replace
puts "Fix: " + name
File.open(name, "w") { |file| file.puts replace }
STDOUT.flush
end
end
Dir[dir + '*/'].each(&method(:find_and_replace))
end
end
+12 -13
View File
@@ -7,7 +7,7 @@
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
@@ -22,6 +22,17 @@
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
@@ -38,17 +49,5 @@
<false/> <false/>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string></string>
<key>NSAppTransportSecurity</key>
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict> </dict>
</plist> </plist>
File diff suppressed because it is too large Load Diff
@@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D2A28121D9B038B00D4039D"
BuildableName = "libReact.a"
BlueprintName = "React-tvOS"
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "drip-tvOS.app"
BlueprintName = "drip-tvOS"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
BuildableName = "drip-tvOSTests.xctest"
BlueprintName = "drip-tvOSTests"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
BuildableName = "drip-tvOSTests.xctest"
BlueprintName = "drip-tvOSTests"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "drip-tvOS.app"
BlueprintName = "drip-tvOS"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "drip-tvOS.app"
BlueprintName = "drip-tvOS"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "drip-tvOS.app"
BlueprintName = "drip-tvOS"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0620"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "83CBBA2D1A601D0E00E9B192"
BuildableName = "libReact.a"
BlueprintName = "React"
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "drip.app"
BlueprintName = "drip"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "dripTests.xctest"
BlueprintName = "dripTests"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "dripTests.xctest"
BlueprintName = "dripTests"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "drip.app"
BlueprintName = "drip"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "drip.app"
BlueprintName = "drip"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "drip.app"
BlueprintName = "drip"
ReferencedContainer = "container:drip.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:drip.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

Some files were not shown because too many files have changed in this diff Show More