Compare commits

..

2 Commits

Author SHA1 Message Date
Sofiya Tepikin 9f3d115aca Use the date from context 2022-09-24 12:44:28 +02:00
Sofiya Tepikin 2f187b1007 Add date context and provide it to the app 2022-09-24 12:44:16 +02:00
146 changed files with 5492 additions and 5894 deletions
+6 -2
View File
@@ -59,9 +59,8 @@ buck-out/
# Bundle artifact
*.jsbundle
# Ruby / CocoaPods
# CocoaPods
/ios/Pods/
/vendor/bundle/
# RN android release
android/app/bin/
@@ -74,6 +73,11 @@ ios/Podfile.lock
android/app/src/main/res/drawable-*
android/app/src/main/assets/*
# nodejs-mobile creates these with every npm install
nodejs-assets/nodejs-project/sample-*
nodejs-assets/build-native-modules-MacOS-helper-script-node.sh
nodejs-assets/build-native-modules-MacOS-helper-script-npm.sh
# yarn
.yarn/*
yarn-error.log
-1
View File
@@ -8,7 +8,6 @@ cache:
test_async:
script:
- npm install npm@7.0.1 -g
- npm install
- npm test
+1 -1
View File
@@ -11,7 +11,7 @@ updates:
- dependency-name: '*'
update-types: ['version-update:semver-patch']
- dependency-name: 'realm'
- dependency-name: 'nodejs-mobile-react-native'
- dependency-name: 'react'
- dependency-name: 'react-native'
- dependency-name: 'react-native-push-notifications'
- dependency-name: '@babel/core'
-29
View File
@@ -1,29 +0,0 @@
## 🐛 Oh no, a bug 🐛
### What happened?
Short overview of how the bug manifests.
### What is the expected behaviour?
_It's supposed to show ... and not ..._
### Which OS + version is your device?
- [ ] Android _number_
- [ ] iOS _number_
- [ ] Simulator _number_
### Which drip version number are you using?
_On your phone, go to ➞ menu on the top right ➞ about, scroll to the very bottom, and find the version number_
### How did it happen?
_What triggered the bug/behavior, always/sometimes, is it reproducible(how)?_
### Describe how it looks or add a screenshot
Feel free to attach a file 📎
### Any ideas on how to solve it? 💡
-23
View File
@@ -1,23 +0,0 @@
## 🪠 This has to be done 🪠
### What has to be done?
Short overview
### Is it urgent? ⏳
- [ ] Yes
- [ ] No
- [ ] Something in between
_Explain the urgency if possible, e.g. is it a security vulnerability for potentially everyone?_
If it is a security vulnerability for potentially everyone, please reach out ASAP to drip@mailbox.org.
### Which OS?
- [ ] Android
- [ ] iOS
### What should the ideal outcome be? 🎆
_You can, e.g., specify the version number for a library update_
@@ -1,26 +0,0 @@
## 🧩 Yeah, a feature idea! 🧩
### This feature is a ...
- [ ] period tracking feature (add more data points etc.)
- [ ] technological feature (password, design, settings, etc.)
### What should this feature do or solve? 🪄
Please give a short overview so as many people as possible would be able to understand.
### Who is this feature for?
### What is particularly important to the people who would use this feature?
### Where in the app should the feature be added?
### Is it connected with or dependent on some other feature?
### How should the feature look (sketch or mock-up)?
Feel free to attach a file 📎
### What could be difficulties (esp. integrating with other components)? 🪆
### Do you want to work on this yourself?
@@ -1,5 +1,7 @@
## Why this change?
Closes ticket #
## Description
I have tested this MR:
+14 -169
View File
@@ -2,173 +2,18 @@
All notable changes to this project will be documented in this file.
## v1.2403.19 Android & iOS
## v1.2102.28
### Changes
- Disables temperature slider if temperature tracking off
- Disables secondary symptom if fertility and or cervix/cervical mucus are off
- Disables temperature reminder if temperature tracking off
- Disabled period reminder if period predictions off
- Return from sympto adapter if fertility off
- Restructure settings menu
- Unify wording to "sympto-thermal method"
- Format decimal to x.0 instead of x.00 used for standard deviation and average cycle in stats
- Use SelectTabGroup for secondary symptom customization
- Android changes after updating React Native to 0.68.3
- Update Android Gradle plugin from 7.0.3 to 7.0.4
- Update NDK to "24.0.8215888" only for M1 users which added support for aarch64
- Update metadata phone screenshots for Fdroid store listing
- Updated dependencies:
- @react-native-community/datetimepicker from 6.3.1 to 7.2.0
- @react-native-async-storage/async-storage from ^1.17.9 to ^1.18.2
- metro-react-native-babel-preset from ^0.66.2 to ^0.67.0
### Adds
- Customization settings can turn on & off:
- Tracking categories
- Period predictions
- Fertility phases calculation
- Home displays text elements depending on customization settings
- Chart displays tracking category elements depending on customization settings
- CycleDay displays tracking category elements and exclude switch depending on customization settings
- Reminder can be disabled depending on customization settings
- Adds disabled and more styling to AppSwitch
- Adds TrackingCategorySwitch
- Adds disabled, more styling and alert to SelectTabGroup
- Adds more marginTop to License page
- Adds info text to Password menu item in Settings
## v1.2401.17 iOS
### Changes
- Updating React Native to 0.68.3
- Minor changes in temperature chart
### Adds
- Temperature scale slider
- out of range warning messages for temperature values
### Fixed
- Order of import buttons
## v1.2312.11 iOS
### Changes
- Chart: Improved readability
- Finer temperature lines and dots
- Enlarge screen space for temperature chart
- A very light grey background color for weekend days on the whole chart screen
- Homescreen: date displayed in new format
- Minor changes in "about" section
See more under v1.2311.14 Android
### Adds
- Stats: Show period details, including cycle start, cycle length and amount of days with bleeding
- Stats: Explainer text for standard deviation
- App asks for permissions for notifications right at the start, which allows you to set reminders (this is a new requirement for Android 13)
## v1.2311.14 Android
### Changes
- Make the app compatible with Android 13
- Update Android's targetSdkVersion to 33
- Update buildToolsVersion to 33.0.2
- Update Android Gradle plugin to 7.0.3
- Update Gradle to 7.3.3
- Update kotlinVersion to 1.3.40
- Chart: Improved readability
- Finer temperature lines and dots
- Enlarge screen space for temperature chart
- A very light grey background color for weekend days on the whole chart screen
- Reminders:
- Use new fork of react-native-push-notification: <https://github.com/github:bl00dymarie/react-native-push-notification> without google services
- Adding channels after breaking changes in react-native-push-notification
- Homescreen: date displayed in new format
- Minor changes in "about" section
- Updated dependencies:
- moment ^2.29.4,
- prop-types ^15.8.1,
- react v17.0.2,
- react-native v0.67.4,
- react-native-calendars ^1.1287.0,
- react-native-document-picker ^8.1.1,
- react-native-fs ^2.20.0,
- react-native-modal-datetime-picker v14.0.0,
- react-native-share ^7.9.0,
- react-native-vector-icons ^9.2.0,
- realm ^10.16.0,
- sympto v3.0.1
### Adds
- Stats: Show period details, including cycle start, cycle length and amount of days with bleeding
- Stats: Explainer text for standard deviation
- Settings: Privacy Policy
- App asks for permissions for notifications right at the start, which allows you to set reminders (this is a new requirement for Android 13)
- Buttons can now be displayed as row
- Added dependencies:
- @js-joda/core ^5.3.0,
- @react-native-async-storage/async-storage ^1.17.9,
- @react-native-community/art ^1.2.0,
- @react-native-community/datetimepicker ^6.3.1,
- @react-native-community/push-notification-ios ^1.11.0,
- i18next ^22.0.2,
- react-i18next ^12.0.0,
- jshashes ^1.0.8,
- react-native-permissions ^3.10.0,
- react-native-push-notification: github:bl00dymarie/react-native-push-notification,
- react-native-simple-toast ^1.1.3,
- react-native-size-matters ^0.4.0,
### Fixed
- Password: Disable setting empty passwords
- After updating the password the app will do a full restart
- Chart: Grid for symptoms
- Chart: Horizontal lines in temperature chart
### Unreleased
- Partially implemented translations with react-i18next
## v1.2208.11 iOS
### Adds
- first iOS Release
### Unreleased
- Temperature scale slider
- Warning message for out of scope temperature values
## v1.2102.28 Android
### Changes
- Temperature range is now between 35 - 39°C and its default values are now set to 35.5 - 37.5°C
### Fixed
- Blocks invalid input of temperature value
- Error message for incorrect password on login screen
- Phase text on home screen for last fertile day
- Styling improvements
## v1.2101.9 Android
## v1.2101.9
### Adds
@@ -211,12 +56,12 @@ See more under v1.2311.14 Android
- Fixed react-native-vector-icon
- Fixed AppLoadingView component centering
## v0.2007-12.beta Android
## v0.2007-12.beta
### Adds
- Allows chart not to show temperature part, when temperature is not tracked and corresponding refactoring
- Detox support for e2e testing and addition of the e2e tests
- Detox support for e2e testing and addition of the e2e tests
- Introduces Redux global state (date and navigation are stored locally now)
- Introduces clear.sh script to the project automising clearing project caches and packages reinstallation
@@ -235,7 +80,7 @@ See more under v1.2311.14 Android
- Fixed adding notes to the future dates
- Fixed app exiting with error when hitting back button on device
- Fixed Sex symptom showing on y axis of chart even though the contraception method was deleted
- Fixed Sex symptom showing on y axis of chart even though the contraception method was deleted
- Fixed of the clear.sh file name in package.json
- Fixed of navigation from chart to the cycle day overview
- Bug fix for maximum value of mucus not showing on chart
@@ -246,7 +91,7 @@ See more under v1.2311.14 Android
- Updates of node.js to fix security issue
## v0.2005.3-beta Android
## v0.2005.3-beta
- Adds arm64-v8a and x86_64 for supporting 64-bit architecture
- Adds Mariya & Sofiya as contributors &lt;3
@@ -346,7 +191,7 @@ See more under v1.2311.14 Android
- Adds release wizard
- Updates nodejs-mobile to fix security issue
## v0.1905.29-beta Android
## v0.1905.29-beta
### Changes
@@ -367,7 +212,7 @@ See more under v1.2311.14 Android
- Line width in chart
- Prediction range in drop on homescreen
## v0.1905.28-beta Android
## v0.1905.28-beta
- Displays all the text for Home Elements; Shortens margin btw Home Elements; Adds missing "visit" to text
- Adds donation section to about
@@ -442,7 +287,7 @@ See more under v1.2311.14 Android
- Nicer formatting for past bleeding prediction
- Fixed prediction range in drop on homescreen
## v0.1905.10-beta Android
## v0.1905.10-beta
- Filter release commits from changelog
- Adds update-changelog script
@@ -467,7 +312,7 @@ See more under v1.2311.14 Android
- Delete password button bug fix
- temperature screen styling update
## v0.0.3 - 2019-04-17 Android
## v0.0.3 - 2019-04-17
### Changes
@@ -477,9 +322,9 @@ See more under v1.2311.14 Android
- Button functionality in settings for password
## v0.0.2 - 2019-04-09 Android
## v0.0.2 - 2019-04-09
## Second updated beta release version Android
## Second updated beta release version
### Changes
@@ -493,9 +338,9 @@ See more under v1.2311.14 Android
- Delete button for sex, pain and mood
- Dates on chart
## v0.0.1 - 2019-02-15 Android
## v0.0.1 - 2019-02-15
## First beta release version Android
## First beta release version
### Added (list of core functionality)
+23 -22
View File
@@ -1,10 +1,10 @@
# Contributing to drip aka CONDRIPUTING
So good to see you here, hello :wave\_tone1: :wave\_tone2: :wave\_tone3: :wave\_tone4: :wave\_tone5:
So good to see you here, hello :wave_tone1: :wave_tone2: :wave_tone3: :wave_tone4: :wave_tone5:
## Table Of Contents
[TL;DR](#tldr)
[TL;DR](#tl-dr)
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
@@ -14,44 +14,45 @@ So good to see you here, hello :wave_tone1: :wave_tone2: :wave_tone3: :wave_tone
## TL;DR
You just want to say hello? Send us a [nice email](mailto:drip@mailbox.org?Subject=Nice%20incoming%20mail) :postbox:, toot :mammoth: [@dripapp@mastodon.social](https://mastodon.social/@dripapp) or tweet :bird: at us [@dripberlin](https://twitter.com/dripberlin).
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?
We have prepared something for **you**: check out our [README](https://gitlab.com/bloodyhealth/drip/blob/main/README.md) for more information on how to set up and install everything you'll need.
[Ping us](mailto:drip@mailbox.org) if you could need some help :helmet_with_cross: !
We have prepared something for **you**: check out our [README](https://gitlab.com/bloodyhealth/drip/blob/master/README.md) for more information on how to set up and install everything you'll need.
[Ping us](mailto:drip@mailbox.org) if you could need some help :helmet\_with\_cross: !
Let us know if you want to suggest improvements for the README and open a merge request (which is just like Github's pull request)
## How can I condripute?
We are an open source project and we highly appreciate contributions. At the same time please keep some things in mind: :heart:
### Your First Code Condripution
- The drip maintainers are usually around 2-3 people and are responsible for everything from developing new features to answering press inquiries or fixing bugs. Most of this work is done unpaid. (If you write a nice email your chances to get an answer are much higher than if you behave like a tech bro.)
- We have a plan for the direction in which drip should evolve. This means that even if it isn't always transparent on gitlab, we have a list of features that we are building and more importantly, which bugs need to be fixed and which dependencies need to be updated.
- drips core values are:
- 🔮 open source
- 🩸 feminist and gender inclusive
- 🔒 secure: data entered stays with that person/on their device
- 🔬 science-based: we implemented the sympto-thermal method
We are fans of labels, at least for our issues. You can find a list of `newbie` issues [here](https://gitlab.com/bloodyhealth/drip/issues?label_name%5B%5D=Newbie).
If you decide to work on an issue, please click on `Create branch` based on that issue. You can find this as a dropdown option right under `Create merge request`.
This allows us to keep track of the issues that are related to an existing branch and tells everyone "somebody is working on it".
This means that we will never implement anything that contradicts these core values. Some examples: We will never build a cloud integration, we will never make an ovulation prediction.
If you want to open a merge request, yeah :tada: exciting! We are using a template for merge requests to make sure we explain what we have done and why.
Keep in mind that people who will review your merge request are more motivated to do so when the merge request is well explained and ideally not too big.
- If you would like to make a sustainable contribution to the project, we would be happy to join the game.
### Reporting Bugs
### Reporting Bugs or Making Suggestions
You found a bug :bug:? Please feel free to investigate and save logs for more specific details.
If you found a bug or have suggestions, please :one: first review the [list of existing issues](https://gitlab.com/bloodyhealth/drip/issues) to make sure the bug/suggestion is unknown; :two: after that, take time to investigate and write a detailed issue report. Best you use our [templates for bug /chore/ feature](https://gitlab.com/bloodyhealth/drip/-/tree/main/.gitlab/issue_templates) (you can choose when creating a new issue) in gitlab or send us an email to our [gitlab email](mailto:incoming+bloodyhealth/drip@incoming.gitlab.com). It will automagically add a new issue to the list with a description text taken from the body of your email. Please attach screenshots or other helpful things.
Before creating a new issue, please review the [list of existing issues](https://gitlab.com/bloodyhealth/drip/issues) to make sure the bug is unknown. You are invited to describe the bug in all its details e.g. in what context/settings has it occurred?
### Your First Condripution
To send us a new issue you can also use our [gitlab email](mailto:incoming+bloodyhealth/drip@incoming.gitlab.com). It will automagically add a new issue to the list with a description text taken from the body of your email.
- If you want to contribute to drip's code or translation, please get in touch with us. We can tell you if someone is already working on something similar, if your idea does (not) match drip's core values, etc. Of course, you can always create a drip version for your private use with anything you want, but please don't expect us to review and merge code that has not been discussed before.
- If you want to open a merge request, yeah :tada: exciting! We are using a template for merge requests to make sure we explain what we have done and why.
- Keep in mind that people who will review your merge request are more motivated to do so when the merge request is well explained and ideally not too big.
### Suggesting Enhancements
Do you have suggestions for enhancing the app or for cleaning up some code? Fantastic!
Before creating a new issue, please review the [list of existing issues](https://gitlab.com/bloodyhealth/drip/issues) to make sure nobody else had the same idea before you! You are then invited to open a new issue with a somewhat extensive description, you can use emojis or GIFs if it helps :)!
To send us a new issue you can also use our [gitlab email](mailto:incoming+bloodyhealth/drip@incoming.gitlab.com). It will automagically add a new issue to the list with the title taken from the subject line and the description text for the issue taken from the body of your email.
### Thank you
![](https://media.giphy.com/media/kPA88elN9kYco/giphy.gif)
Thank you for condriputing to open source, thank you for condriputing to drip!
Much love from Bloody Health :heart_exclamation:
Much love from Bloody Health :heart\_exclamation:
+96
View File
@@ -0,0 +1,96 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.5.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.1.9)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.6.1)
minitest (5.15.0)
molinillo (0.8.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.6)
rexml (3.2.5)
ruby-macho (2.5.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.5.4)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (~> 1.11, >= 1.11.2)
RUBY VERSION
ruby 2.7.4p191
BUNDLED WITH
2.2.27
+23 -47
View File
@@ -3,24 +3,21 @@
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://dripapp.org/).
[<img src="https://dripapp.org/assets/get.png"
[<img src="https://dripapp.org/assets/get.png"
alt="Get it here"
height="55">](https://dripapp.org/release/v1.2311.14.apk)
[<img src="https://dripapp.org/assets/badge_google-play.png"
alt="Get it on Google Play"
height="55">](https://dripapp.org/android)
[<img src="https://dripapp.org/assets/badge_app-store.png"
alt="Get drip on the App Store"
height="55">](https://dripapp.org/ios)
[<img src="https://dripapp.org/assets/badge_f-droid.png"
height="55">](https://dripapp.org/release/8.apk)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="55">](https://dripapp.org/fdroid)
height="80">](https://f-droid.org/packages/com.drip/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.drip)
The app is built in React Native and developed for Android and iOS.
The app is built in React Native and currently developed for Android.
▶ [How to contribute to the project](https://gitlab.com/bloodyhealth/drip/blob/main/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/main/RELEASE.md)
▶ [How to release a new version](https://gitlab.com/bloodyhealth/drip/blob/master/RELEASE.md)
## Development setup
@@ -36,14 +33,14 @@ or clone it with HTTPS
### 2. Node & yarn version
Make sure you are running Node 14 and classic yarn (v.1). It's easiest to switch Node versions using `nvm`, here's how to install NVM: https://github.com/nvm-sh/nvm#installing-and-updating. After installing nvm close the terminal and open it again to be able to use nvm.
Once you have nvm running you can install node 14:
Make sure you are running Node 14 and classic yarn (v.1). It's easiest to switch Node versions using `nvm`, here's how to install NVM: https://github.com/nvm-sh/nvm#installing-and-updating. Once you have nvm you can install node 14:
nvm install v14.19.3
use npm to install yarn:
and then run
npm install --global yarn
cd drip
yarn install
## for Android
@@ -53,46 +50,21 @@ Install [Android Studio](https://developer.android.com/studio/) - you'll need it
### 3.2 More requirements from Android Studio
Open Android Studio. If the message "SDK location not found" appears when you try to start it, edit `.bashrc` in your home directory by adding:
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
Check by typing in your terminal:
echo $ANDROID_SDK_ROOT
You should see the path of Sdk.
If you haven't installed [adb tools](https://developer.android.com/tools/adb) before, you will also have to do so and add to the .bashrc
PATH="$PATH:$HOME/Android/Sdk/platform-tools"
In Android Studio 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.
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.
### 3.3 Run the app on Android
Either create and 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.
i. Open a terminal, navigate to the drip folder and run
i. Open a terminal and run
yarn install
In a separate tab, that needs to keep running as long as you want to keep the app connected, run
yarn start
Also run in a new tab
yarn android
ii. To see logging output, run the following command in another tab:
yarn log
iii. If you had an older version of drip before and you are now trying to run a new drip version, clear cache by running
yarn clear
iv. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html):
iii. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html):
adb shell input keyevent 82
@@ -175,7 +147,7 @@ You can run the tests with:
### End to end tests
1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/main/package.json) under:
1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/master/package.json) under:
```
{"detox":
{"configurations":
@@ -229,3 +201,7 @@ More information about how the app calculates fertility status and bleeding pred
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.
## Translation
We are using [Weblate](https://weblate.org/) as translation software.
+17 -167
View File
@@ -1,183 +1,33 @@
# How to release
# How to release a new version
drip is developed in React Native for iOS and Android and is released on 4 different platforms:
Note: You need the release-key to bundle a release that can be uploaded to Google Play Store.
1. [Google Play Store](https://play.google.com/store/apps/details?id=com.drip)
2. [Apple App Store](https://apps.apple.com/us/app/drip/id1584564949)
3. [F-Droid](https://f-droid.org/packages/com.drip/)
4. [drip Website](https://dripapp.org)
In an ideal world the app version is the same across platforms. In reality this has never been the case.
Releasing a new version is very exciting and brings happy changes like fixing a bug, improving a feature, updating dependencies or adding a new functionality to the app. It is more than just pressing the button "publish new version".
_Note_: You need the release-key for Android to bundle a signed release that can be uploaded and published via the Google Play Store. A similar process for Apple requires a certificate to upload and publish the app to the App Store.
### Release steps
1. [Version updating](#version-updating)
2. [Android builds](#android-builds)
3. [iOS builds](#ios-builds)
4. [User testing](#user-testing)
5. [Changelog](#changelog)
6. [Release notes](#release-notes)
7. [Release tag](#release-tag)
8. [Phone screenshots](#phone-screenshots)
9. [Publishing](#publishing)
10. [Communication](#communication)
11. [Self care](#self-care)
## Version updating
When you are done with a chore, a feature or a bugfix, you may want to share it with testers and eventually publish a release. In order to identify a specific app version we can update the version name, which is created based on the following format: `1.yymm.d` e.g. `1.2311.7`. If you want to upload a new app version to Google Play you also need to update the version code.
The following command will:
- create a new versionName and a new higher versionCode (+1)
- create a commit including a tag named after the new release version name.
Run the release wizard that takes you through all the steps necessary to prepare a new release:
```
yarn release
npm run release
```
This will trigger the following:
* update version number
* create a new tag for the release
* update the changelog
* make a release commit
The versionName and versionCode [are defined here](https://gitlab.com/bloodyhealth/drip/-/blob/5401789c46f4a02915ab900ef284581be420451c/android/app/build.gradle#L137-138) and in [package.json](https://gitlab.com/bloodyhealth/drip/-/blob/5401789c46f4a02915ab900ef284581be420451c/package.json#L3).
**Note for iOS**
Update the version number for iOS in `ios/drip/Info.plist` under:
To then bundle a release run the following command on your branch:
```
<key>CFBundleShortVersionString</key>
<string>1.2403.19</string>
cd android && ./gradlew bundleRelease
```
### Android builds
This command creates an `app.aab` file in the folder `/android/app/build/outputs/bundle/release`.
APK versus AAB
[More on Android App Bundle](https://blog.swmansion.com/make-your-react-native-app-3x-smaller-44c993eda2c9)
> Android App Bundles (AAB) include all your apps compiled code and resources, but defer APK generation and signing to Google Play. Unlike an APK, you can't deploy an app bundle directly to a device. So, if you want to quickly test or share an APK with someone else, you should instead build an APK.
(https://developer.android.com/build/building-cmdline)
#### APK
To build a release apk file, run the following command:
You need to manually push the created tag to Gitlab:
```
yarn build-android-apk-release
git push origin <tagname>
```
Also don't forget to push your branch to Gitlab and review and merge it if ready!
_which is a shortcut for:_ `cd android && ./gradlew clean && ./gradlew assembleRelease && cd ..`
This will create a new apk file named `app-release.apk` under `./android/app/build/outputs/apk/release/`.
For signing an apk you can run this command:
```
yarn sign-android-apk-release
```
_which is a shortcut for:_ `zipalign -v -p 4 ./android/app/build/outputs/apk/release/app-release.apk ./android/app/build/outputs/apk/release/app-release_signed.apk`
It adds a file name `app-release_signed.apk` in the same folder in `./android/app/build/outputs/apk/release/`
#### AAB
To build a release aab file, run:
```
yarn build-android-aab-release
```
_which is a shortcut for:_ `cd android && ./gradlew clean && ./gradlew :app:bundleRelease && cd ..`
It creates a new aab file named `app-release.aab` under `./android/app/build/outputs/bundle/release`
For signing an aab you first need to configure the base modules build.gradle file with your apps signing information. You can then run this command:
```
yarn sign-android-aab-release
```
_which is a shortcut for:_ `jarsigner -keystore ./android/app/drip-release-key.keystore ./android/app/build/outputs/bundle/release/app-release.aab drip-release-key`
### iOS builds
To build an .ipa archive file for an upload to the AppStore you need to go to xCode and select Build -> "Any iOS Device" and under "Product" -> "Archive".
Once the archiving process has completed you can chose to do the following:
"Distribute the app"
- TestFlight & App Store for when you want to upload it for external testing and/or production release
- TestFlight Internal Only for when you want to upload it for internal testing
## User testing
To enable external testing you need to remember that Google Play and Apple App Store might take up to 1 day for their review process. "External testing" for iOS allows testing drip on Testflight anonymously via a public link. "Open testing" for Android allows testing drip on Google Play as beta tester below the normal production listing.
For a quick and easy way to share an apk to testers who are willing to sideload drip onto their Android phones, do this: Upload a signed apk to the Gitlab repository of the drip website under `/release` https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/tree/main/release and maybe adapt the name of the apk with a more specific name than "app-release.apk". Now you can simply share a direct link to download your newly bundled apk, e.g. [a download link for v1.2311.14](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/blob/main/release/v1.2311.14.apk).
## Changelog
The [changelog](https://gitlab.com/bloodyhealth/drip/-/blob/main/CHANGELOG.md) should reflect the technical / code changes between a previous and the new version. Please update the changelog file with any relevant additions, fixes and changes in the following format:
**v1.yymm.d**
**Changes**
Changing the color of funky button
Updating a library from 1.2.3 to 2.3.4
**Adds**
New feature for calendar
**Fixed**
Small bug in chart
## Release notes
These notes are for the users and curious ones who may want to start using drip. They should be based on the changelog but written in a friendly and easy to understand way. The focus is on the user perspective and the impact of the changes for the user. Behind the scenes and in depth code changes are less relevant.
Google Play limits these notes to 500 characters, whereas Apple's App Store limits these notes to 4.000 characters. In Fdroid there are no release notes.
## Release tag
[Tags](https://gitlab.com/bloodyhealth/drip/-/tags) can mark a specific point in the coding/commmit history and helps us identify the version status of a released app. They are named "iOS-v1.2401.17" or "Release-v1.yymm.d".
## Phone screenshots
If there are visual changes in the app you may want to update the screenshots for the Google Play Store listing, which allows up to 8 and for Apple's App Store, which allows up to 10 screenshots. Keep in mind that both Google Play and Apple have specific resolution requirements. You'll find Google's in Grow -> Store presence -> Main Store Listing -> Phone screenshots and Apple's on the main App Store Connect site. Here is a link for [Apple's screenshot specifications](https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications).
Please also update [phone screenshots for the website](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/tree/main/assets) and set links on [/index](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/blob/f3da9776b1943ffa32458e74ef86eeca98c1891c/index.html#L47) and [/media](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/blob/c7f999bb7ad736345321537cbffa3f4c24eeee6d/media.html#L33) that can then also be attached to a social media post.
## Publishing
### Google Play Console
Upload a signed aab to the [Google Play Console for developers](https://play.google.com/console/) and add it to the "App bundle explorer". This requires a higher versionCode and a different version name compared to previously uploaded aab or apk files.
You can decide if you want the new app version to get released for testing (internal, closed or open) or for production. Keep in mind that any track other than "internal testing" triggers an external review by Google and might take a few hours.
### Apple App Store Connect
Upload a new version and submit it for review, before it can be published.
### drip website
After a new version has been published on Google Play (or F-Droid) the apk version that is downloadable directly from the [drip website](https://dripapp.org) needs to get updated as well. Therefore you upload a signed apk to the [repository](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/) as [we did in this commit](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/commit/f8c0f90c1ae9f23bf8e1bc311790b85443149a4d), and adapt the name and link on /index.html [as we did in this commit](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/commit2f8850ff5fa78615a4f335b625ea4a67d4acf03a) and [this commit](https://gitlab.com/bloodyhealth/bloodyhealth.gitlab.io/-/commit/f3da9776b1943ffa32458e74ef86eeca98c1891c). Last time I checked it was [here](f3da9776b1943ffa32458e74ef86eeca98c1891c/index.html#L114).
## Communication
You probably want to share the app update by posting on one or more of these platforms:
- [Twitter](twitter.com/dripberlin)
- [Mastodon](mastodon.social/@dripapp)
- [Ko-fi](https://ko-fi.com/dripapp)
- [Linkedin](https://www.linkedin.com/company/34899684/)
- Different tech, privacy, feminist oriented slacks
## Self care
Congratulations. Take a break, eat some chocolate, go see a live show of your favorite band, masturbate <3!
Yay, done (have a scoop of ice cream, I suggest coconut 🍦)!
+7 -8
View File
@@ -134,10 +134,10 @@ android {
applicationId "com.drip"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 33
versionName "1.2403.19"
versionCode 8
versionName "1.2102.28"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
abiFilters "armeabi-v7a", "x86", "arm64-v8a"
}
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -198,7 +198,7 @@ android {
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
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)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
@@ -211,12 +211,11 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "androidx.appcompat:appcompat:1.0.0"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
+1 -1
View File
@@ -8,6 +8,6 @@
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
@@ -1,10 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.drip;
package com.rndiffapp;
import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
@@ -18,7 +18,6 @@ import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.react.ReactInstanceEventListener;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
@@ -47,7 +46,7 @@ public class ReactNativeFlipper {
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceEventListener() {
new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
reactInstanceManager.removeReactInstanceEventListener(this);
+50 -57
View File
@@ -5,81 +5,74 @@
>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<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.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:name=".MainApplication"
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="false" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/purple" />
<meta-data
android:name="com.dieam.reactnativepushnotification.default_notification_channel_id"
android:value="..." />
<receiver
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions"
android:exported="false"
tools:ignore="MissingClass" />
<receiver
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher"
android:exported="false" />
<receiver
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"
android:exported="false" >
<intent-filter>
<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>
</receiver>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="sensorPortrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="sensorPortrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.drip.provider"
android:grantUriPermissions="true"
android:exported="false" >
android:exported="false">
<meta-data
tools:replace="android:resource"
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</provider>
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="false"/>
<!-- 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"
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.RNPushNotificationBootEventReceiver">
<intent-filter>
<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>
</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>
</manifest>
@@ -1,8 +1,6 @@
package com.drip;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
public class MainActivity extends ReactActivity {
@@ -14,27 +12,4 @@ public class MainActivity extends ReactActivity {
protected String getMainComponentName() {
return "drip";
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and
* you can specify the rendered you wish to use (Fabric or the older renderer).
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new MainActivityDelegate(this, getMainComponentName());
}
public static class MainActivityDelegate extends ReactActivityDelegate {
public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
super(activity, mainComponentName);
}
@Override
protected ReactRootView createRootView() {
ReactRootView reactRootView = new ReactRootView(getContext());
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
return reactRootView;
}
}
}
@@ -8,7 +8,6 @@ import com.facebook.react.ReactInstanceManager;
import cl.json.ShareApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
@@ -63,7 +62,7 @@ public class MainApplication extends Application implements ReactApplication, Sh
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.drip.ReactNativeFlipper");
Class<?> aClass = Class.forName("com.rndiffapp.ReactNativeFlipper");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
@@ -14,5 +14,4 @@
<color name="grey">#A5A5A5</color>
<color name="orange">#F38337</color>
<color name="purple">#3A2671</color>
<color name="turquoiseDark">#69CBC1</color>
</resources>
+11 -18
View File
@@ -1,5 +1,3 @@
import org.apache.tools.ant.taskdefs.condition.Os
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
@@ -7,14 +5,13 @@ buildscript {
google()
mavenCentral()
}
ext.kotlinVersion = '1.3.40'
ext.kotlinVersion = "1.3.10"
dependencies {
classpath('com.android.tools.build:gradle:7.0.4')
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("de.undercouch:gradle-download-task:4.1.2")
classpath("com.android.tools.build:gradle:4.2.2")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
@@ -49,16 +46,12 @@ allprojects {
}
ext {
buildToolsVersion = "33.0.2"
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
googlePlayServicesVersion = "+" // default: "+"
firebaseMessagingVersion = "21.1.0" // default: "+"
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888"
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = "21.4.7075529"
}
buildToolsVersion = "30.0.2"
minSdkVersion = 23
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "21.4.7075529"
}
+5 -14
View File
@@ -9,8 +9,8 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -25,16 +25,7 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.125.0
FLIPPER_VERSION=0.99.0
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# https://github.com/facebook/react-native/issues/30729
org.gradle.jvmargs=-Xmx4g
Binary file not shown.
+2 -2
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+114 -162
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#!/usr/bin/env bash
#
# Copyright © 2015-2021 the original authors.
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,103 +17,70 @@
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
MAX_FD="maximum"
warn () {
warn ( ) {
echo "$*"
} >&2
}
die () {
die ( ) {
echo
echo "$*"
echo
exit 1
} >&2
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -121,9 +88,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD=$JAVA_HOME/bin/java
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -132,7 +99,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -140,95 +107,80 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$( cygpath --unix "$JAVACMD" )
JAVACMD=`cygpath --unix "$JAVACMD"`
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
-7
View File
@@ -3,10 +3,3 @@ rootProject.name = 'drip'
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':app'
includeBuild('../node_modules/react-native-gradle-plugin')
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
include(":ReactAndroid")
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
}
+9 -25
View File
@@ -14,30 +14,26 @@ import {
determinePredictionText,
formatWithOrdinalSuffix,
} from './helpers/home'
import {
fertilityTrackingObservable,
periodPredictionObservable,
} from '../local-storage'
import { Colors, Fonts, Sizes, Spacing } from '../styles'
import { LocalDate } from '@js-joda/core'
import { useTranslation } from 'react-i18next'
import { useDate } from '../hooks/useDate'
const Home = ({ navigate, setDate }) => {
const Home = ({ navigate }) => {
const { t } = useTranslation()
const { setDate } = useDate()
function navigateToCycleDayView() {
setDate(todayDateString)
navigate('CycleDay')
}
const isFertilityTrackingEnabled = fertilityTrackingObservable.value
const todayDateString = LocalDate.now().toString()
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
const cycleDayNumber = getCycleDayNumber(todayDateString)
const { status, phase, statusText } =
isFertilityTrackingEnabled && getFertilityStatusForDay(todayDateString)
const isPeriodPredictionEnabled = periodPredictionObservable.value
getFertilityStatusForDay(todayDateString)
const prediction = determinePredictionText(getPredictedMenses(), t)
const cycleDayText = cycleDayNumber
@@ -51,7 +47,6 @@ const Home = ({ navigate, setDate }) => {
>
<AppText style={styles.title}>{moment().format('MMM Do YYYY')}</AppText>
{/* display if at least 1 bleeding day has been entered */}
{cycleDayNumber && (
<View style={styles.line}>
<AppText style={styles.whiteSubtitle}>{cycleDayText}</AppText>
@@ -60,9 +55,7 @@ const Home = ({ navigate, setDate }) => {
</AppText>
</View>
)}
{/* display if fertility tracking enabled and if phase 1, 2 or 3 has been identified */}
{isFertilityTrackingEnabled && phase && (
{phase && (
<View style={styles.line}>
<AppText style={styles.whiteSubtitle}>
{formatWithOrdinalSuffix(phase)}
@@ -74,18 +67,13 @@ const Home = ({ navigate, setDate }) => {
<Asterisk />
</View>
)}
{isPeriodPredictionEnabled && (
<View style={styles.line}>
<AppText style={styles.turquoiseText}>{prediction}</AppText>
</View>
)}
{!isFertilityTrackingEnabled && <View style={styles.largePadding}></View>}
<View style={styles.line}>
<AppText style={styles.turquoiseText}>{prediction}</AppText>
</View>
<Button isCTA isSmall={false} onPress={navigateToCycleDayView}>
{t('labels.home.addDataForToday')}
</Button>
{phase && <Footnote colorLabel="greyLight">{statusText}</Footnote>}
{phase && <Footnote>{statusText}</Footnote>}
</ScrollView>
)
}
@@ -120,14 +108,10 @@ const styles = StyleSheet.create({
color: 'white',
fontSize: Sizes.subtitle,
},
largePadding: {
padding: Spacing.large,
},
})
Home.propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func,
}
export default Home
+7 -1
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
import nodejs from 'nodejs-mobile-react-native'
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
import { openDb } from '../db'
@@ -9,6 +10,8 @@ import AppStatusBar from './common/app-status-bar'
import AcceptLicense from './AcceptLicense'
import PasswordPrompt from './password-prompt'
import { DateProvider } from '../hooks/useDate'
export default function AppWrapper() {
const [isLoading, setIsLoading] = useState(true)
const [isLicenseAccepted, setIsLicenseAccepted] = useState(false)
@@ -27,6 +30,7 @@ export default function AppWrapper() {
}
useEffect(() => {
nodejs.start('main.js')
checkIsLicenseAccepted()
checkIsDbEncrypted()
}, [])
@@ -45,7 +49,9 @@ export default function AppWrapper() {
{isDbEncrypted ? (
<PasswordPrompt enableShowApp={() => setIsDbEncrypted(false)} />
) : (
<App restartApp={() => checkIsDbEncrypted()} />
<DateProvider>
<App restartApp={() => checkIsDbEncrypted()} />
</DateProvider>
)}
</>
)
+3 -4
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import { BackHandler, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { LocalDate } from '@js-joda/core'
import { useDate } from '../hooks/useDate'
import Header from './header'
import Menu from './menu'
@@ -13,7 +13,8 @@ import setupNotifications from '../lib/notifications'
import { closeDb } from '../db'
const App = ({ restartApp }) => {
const [date, setDate] = useState(LocalDate.now().toString())
const { setDate } = useDate()
const [currentPage, setCurrentPage] = useState('Home')
const goBack = () => {
if (currentPage === 'Home') {
@@ -43,8 +44,6 @@ const App = ({ restartApp }) => {
const isTemperatureEditView = currentPage === 'TemperatureEditView'
const headerProps = { navigate: setCurrentPage }
const pageProps = {
date,
setDate,
isTemperatureEditView,
navigate: setCurrentPage,
}
+4 -2
View File
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types'
import { StyleSheet, View } from 'react-native'
import { CalendarList } from 'react-native-calendars'
import { useDate } from '../hooks/useDate'
import { getBleedingDaysSortedByDate } from '../db'
import cycleModule from '../lib/cycle'
import {
@@ -12,7 +14,8 @@ import {
todayToCalFormat,
} from './helpers/calendar'
const CalendarView = ({ setDate, navigate }) => {
const CalendarView = ({ navigate }) => {
const { setDate } = useDate()
const bleedingDays = getBleedingDaysSortedByDate()
const predictedMenses = cycleModule().getPredictedMenses()
@@ -49,7 +52,6 @@ const styles = StyleSheet.create({
})
CalendarView.propTypes = {
setDate: PropTypes.func.isRequired,
navigate: PropTypes.func.isRequired,
}
+3 -12
View File
@@ -4,19 +4,15 @@ import { StyleSheet, View } from 'react-native'
import AppText from '../common/app-text'
import { Sizes, Typography } from '../../styles'
import { Typography } from '../../styles'
import { CHART_YAXIS_WIDTH } from '../../config'
import { shared as labels } from '../../i18n/en/labels'
const ChartLegend = ({ height }) => {
return (
<View style={[styles.container, { height }]}>
<View style={[styles.singleLabelContainer, { height: height / 2 }]}>
<AppText style={styles.textBold}>#</AppText>
</View>
<View style={[styles.singleLabelContainer, { height: height / 2 }]}>
<AppText style={styles.text}>{labels.date}</AppText>
</View>
<AppText style={styles.textBold}>#</AppText>
<AppText style={styles.text}>{labels.date}</AppText>
</View>
)
}
@@ -31,13 +27,8 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end',
width: CHART_YAXIS_WIDTH,
},
singleLabelContainer: {
justifyContent: 'space-around',
alignItems: 'center',
},
text: {
...Typography.label,
fontSize: Sizes.footnote,
},
textBold: {
...Typography.labelBold,
+10 -48
View File
@@ -9,22 +9,11 @@ import HorizontalGrid from './horizontal-grid'
import MainGrid from './main-grid'
import NoData from './no-data'
import NoTemperature from './no-temperature'
import Tutorial from './Tutorial'
import Tutorial from './tutorial'
import YAxis from './y-axis'
import { getCycleDaysSortedByDate } from '../../db'
import {
getChartFlag,
setChartFlag,
desireTrackingCategoryObservable,
moodTrackingCategoryObservable,
noteTrackingCategoryObservable,
painTrackingCategoryObservable,
sexTrackingCategoryObservable,
temperatureTrackingCategoryObservable,
mucusTrackingCategoryObservable,
cervixTrackingCategoryObservable,
} from '../../local-storage'
import { getChartFlag, setChartFlag } from '../../local-storage'
import { makeColumnInfo } from '../helpers/chart'
import {
@@ -39,8 +28,8 @@ import { Spacing } from '../../styles'
const getSymptomsFromCycleDays = (cycleDays) =>
SYMPTOMS.filter((symptom) => cycleDays.some((cycleDay) => cycleDay[symptom]))
const CycleChart = ({ navigate, setDate }) => {
const [shouldShowHint, setShouldShowHint] = useState(false)
const CycleChart = ({ navigate }) => {
const [shouldShowHint, setShouldShowHint] = useState(true)
useEffect(() => {
let isMounted = true
@@ -71,31 +60,7 @@ const CycleChart = ({ navigate, setDate }) => {
(symptom) => symptom !== 'temperature'
)
const symptomRowEnabledSymptoms = symptomRowSymptoms.filter((symptom) => {
if (symptom === 'sex') {
return sexTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'mucus') {
return mucusTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'cervix') {
return cervixTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'desire') {
return desireTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'pain') {
return painTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'mood') {
return moodTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'note') {
return noteTrackingCategoryObservable.value ? symptom : null
} else {
return symptom
}
})
const isTemperatureEnabled = temperatureTrackingCategoryObservable.value
const shouldShowTemperatureColumn =
isTemperatureEnabled && chartSymptoms.indexOf('temperature') > -1
const shouldShowNoDataWarning =
isTemperatureEnabled && chartSymptoms.indexOf('temperature') <= -1
const shouldShowTemperatureColumn = chartSymptoms.indexOf('temperature') > -1
const { width, height } = Dimensions.get('window')
const numberOfColumnsToRender = Math.round(width / CHART_COLUMN_WIDTH)
@@ -106,9 +71,8 @@ const CycleChart = ({ navigate, setDate }) => {
remainingHeight * CHART_SYMPTOM_HEIGHT_RATIO
)
const symptomRowHeight =
PixelRatio.roundToNearestPixel(
symptomRowEnabledSymptoms.length * symptomHeight
) + CHART_GRID_LINE_HORIZONTAL_WIDTH
PixelRatio.roundToNearestPixel(symptomRowSymptoms.length * symptomHeight) +
CHART_GRID_LINE_HORIZONTAL_WIDTH
const columnHeight = remainingHeight - symptomRowHeight
const chartHeight = shouldShowTemperatureColumn
@@ -120,12 +84,11 @@ const CycleChart = ({ navigate, setDate }) => {
const renderColumn = ({ item }) => {
return (
<DayColumn
setDate={setDate}
dateString={item}
navigate={navigate}
symptomHeight={symptomHeight}
columnHeight={columnHeight}
symptomRowSymptoms={symptomRowEnabledSymptoms}
symptomRowSymptoms={symptomRowSymptoms}
chartSymptoms={chartSymptoms}
shouldShowTemperatureColumn={shouldShowTemperatureColumn}
xAxisHeight={xAxisHeight}
@@ -146,11 +109,11 @@ const CycleChart = ({ navigate, setDate }) => {
>
<View style={styles.chartContainer}>
{shouldShowHint && <Tutorial onClose={hideHint} />}
{shouldShowNoDataWarning && <NoTemperature />}
{!shouldShowTemperatureColumn && <NoTemperature />}
<View style={styles.chartArea}>
<YAxis
height={columnHeight}
symptomsToDisplay={symptomRowEnabledSymptoms}
symptomsToDisplay={symptomRowSymptoms}
symptomsSectionHeight={symptomRowHeight}
shouldShowTemperatureColumn={shouldShowTemperatureColumn}
xAxisHeight={xAxisHeight}
@@ -172,7 +135,6 @@ const CycleChart = ({ navigate, setDate }) => {
CycleChart.propTypes = {
navigate: PropTypes.func,
setDate: PropTypes.func,
}
const styles = StyleSheet.create({
+7 -20
View File
@@ -19,20 +19,11 @@ const CycleDayLabel = ({ height, date }) => {
return (
<View style={[styles.container, { height }]}>
<View style={{ ...styles.labelRow, height: height / 2 }}>
<AppText style={styles.textBold}>{cycleDayLabel}</AppText>
</View>
<View style={{ ...styles.labelRow, height: height / 2 }}>
{isFirstDayOfMonth && (
<AppText style={styles.textFootnote}>
{momentDate.format('MMM')}
</AppText>
)}
{!isFirstDayOfMonth && (
<AppText style={styles.textSmall}>{dayOfMonth}</AppText>
)}
<AppText style={styles.textBold}>{cycleDayLabel}</AppText>
<View style={styles.dateLabel}>
<AppText style={styles.text}>
{isFirstDayOfMonth ? momentDate.format('MMM') : dayOfMonth}
</AppText>
{!isFirstDayOfMonth && (
<AppText style={styles.textLight}>
{getOrdinalSuffix(dayOfMonth)}
@@ -54,21 +45,17 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end',
left: 4,
},
textSmall: {
text: {
...Typography.label,
fontSize: Sizes.small,
},
textFootnote: {
...Typography.label,
fontSize: Sizes.footnote,
},
textBold: {
...Typography.labelBold,
},
textLight: {
...Typography.labelLight,
},
labelRow: {
dateLabel: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
+3 -7
View File
@@ -1,7 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { TouchableOpacity } from 'react-native'
import moment from 'moment'
import { getCycleDay } from '../../db'
@@ -9,6 +8,8 @@ import SymptomCell from './symptom-cell'
import TemperatureColumn from './temperature-column'
import CycleDayLabel from './cycle-day-label'
import { useDate } from '../../hooks/useDate'
import {
symptomColorMethods,
getTemperatureProps,
@@ -20,15 +21,13 @@ const DayColumn = ({
dateString,
chartSymptoms,
columnHeight,
setDate,
navigate,
shouldShowTemperatureColumn,
symptomHeight,
symptomRowSymptoms,
xAxisHeight,
}) => {
const momentDate = moment(dateString)
const isWeekend = momentDate.day() == 0 || momentDate.day() == 6
const { setDate } = useDate()
const cycleDayData = getCycleDay(dateString)
let data = {}
@@ -76,7 +75,6 @@ const DayColumn = ({
isVerticalLine={fhmAndLtl.drawFhmLine}
data={data && data.temperature}
columnHeight={columnHeight}
isWeekend={isWeekend}
/>
)}
@@ -96,7 +94,6 @@ const DayColumn = ({
isSymptomDataComplete={
hasSymptomData && isSymptomDataComplete(symptom, dateString)
}
isWeekend={isWeekend}
height={symptomHeight}
/>
)
@@ -110,7 +107,6 @@ DayColumn.propTypes = {
chartSymptoms: PropTypes.array,
columnHeight: PropTypes.number.isRequired,
navigate: PropTypes.func.isRequired,
setDate: PropTypes.func.isRequired,
shouldShowTemperatureColumn: PropTypes.bool,
symptomHeight: PropTypes.number.isRequired,
symptomRowSymptoms: PropTypes.array,
+6 -7
View File
@@ -7,8 +7,7 @@ import { Colors } from '../../styles'
import {
CHART_COLUMN_WIDTH,
CHART_COLUMN_MIDDLE,
CHART_DOT_RADIUS_SYMPTOM,
CHART_DOT_RADIUS_TEMPERATURE,
CHART_DOT_RADIUS,
CHART_STROKE_WIDTH,
} from '../../config'
@@ -36,9 +35,9 @@ const DotAndLine = ({
}
const dot = new Path()
.moveTo(CHART_COLUMN_MIDDLE, y - CHART_DOT_RADIUS_TEMPERATURE)
.arc(0, CHART_DOT_RADIUS_TEMPERATURE * 2, CHART_DOT_RADIUS_TEMPERATURE)
.arc(0, CHART_DOT_RADIUS_TEMPERATURE * -2, CHART_DOT_RADIUS_TEMPERATURE)
.moveTo(CHART_COLUMN_MIDDLE, y - CHART_DOT_RADIUS)
.arc(0, CHART_DOT_RADIUS * 2, CHART_DOT_RADIUS)
.arc(0, CHART_DOT_RADIUS * -2, CHART_DOT_RADIUS)
const dotColor = exclude ? Colors.turquoise : Colors.turquoiseDark
const lineColorLeft = excludeLeftLine
? Colors.turquoise
@@ -59,13 +58,13 @@ const DotAndLine = ({
d={lineRight}
stroke={lineColorRight}
strokeWidth={CHART_STROKE_WIDTH}
key={y + CHART_DOT_RADIUS_SYMPTOM}
key={y + CHART_DOT_RADIUS}
/>
<Shape
d={dot}
stroke={dotColor}
strokeWidth={CHART_STROKE_WIDTH}
fill={dotColor}
fill="white"
key="dot"
/>
</React.Fragment>
+7 -24
View File
@@ -5,7 +5,7 @@ import { StyleSheet, View } from 'react-native'
import { Colors, Containers } from '../../styles'
import {
CHART_COLUMN_WIDTH,
CHART_DOT_RADIUS_SYMPTOM,
CHART_DOT_RADIUS,
CHART_GRID_LINE_HORIZONTAL_WIDTH,
} from '../../config'
@@ -15,31 +15,14 @@ const SymptomCell = ({
symptom,
symptomValue,
isSymptomDataComplete,
isWeekend,
}) => {
const shouldDrawDot = symptomValue !== false
// Determine the background color based on isWeekend prop
const backgroundColor = isWeekend ? Colors.greyVeryLight : 'white'
const styleCell =
index !== 0
? [
styles.cell,
{
height,
width: CHART_COLUMN_WIDTH,
backgroundColor: backgroundColor,
},
]
: [
styles.cell,
{
height,
width: CHART_COLUMN_WIDTH,
backgroundColor: backgroundColor,
},
styles.topBorder,
]
? [styles.cell, { height, width: CHART_COLUMN_WIDTH }]
: [styles.cell, { height, width: CHART_COLUMN_WIDTH }, styles.topBorder]
let styleDot
if (shouldDrawDot) {
const styleSymptom = Colors.iconColors[symptom]
const symptomColor = styleSymptom.shades[symptomValue]
@@ -64,11 +47,11 @@ SymptomCell.propTypes = {
symptom: PropTypes.string,
symptomValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
isSymptomDataComplete: PropTypes.bool,
isWeekend: PropTypes.bool,
}
const styles = StyleSheet.create({
cell: {
backgroundColor: 'white',
borderBottomColor: Colors.grey,
borderBottomWidth: CHART_GRID_LINE_HORIZONTAL_WIDTH,
borderLeftColor: Colors.grey,
@@ -80,8 +63,8 @@ const styles = StyleSheet.create({
borderTopWidth: CHART_GRID_LINE_HORIZONTAL_WIDTH,
},
dot: {
width: CHART_DOT_RADIUS_SYMPTOM * 2,
height: CHART_DOT_RADIUS_SYMPTOM * 2,
width: CHART_DOT_RADIUS * 2,
height: CHART_DOT_RADIUS * 2,
borderRadius: 50,
},
})
+8 -5
View File
@@ -1,6 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Colors } from '../../styles'
import { StyleSheet } from 'react-native'
import { Surface, Path } from '@react-native-community/art'
@@ -14,16 +14,14 @@ const TemperatureColumn = ({
isVerticalLine,
data,
columnHeight,
isWeekend,
}) => {
const x = CHART_STROKE_WIDTH / 2
const backgroundColor = isWeekend ? Colors.greyVeryLight : 'white'
return (
<Surface
width={CHART_COLUMN_WIDTH}
height={columnHeight}
style={{ backgroundColor: backgroundColor }}
style={styles.container}
>
<ChartLine path={new Path().lineTo(0, columnHeight)} />
@@ -65,7 +63,12 @@ TemperatureColumn.propTypes = {
isVerticalLine: PropTypes.bool,
data: PropTypes.object,
columnHeight: PropTypes.number,
isWeekend: PropTypes.bool,
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
},
})
export default TemperatureColumn
+2 -5
View File
@@ -8,15 +8,13 @@ import { Sizes } from '../../styles'
import { CHART_TICK_WIDTH } from '../../config'
const Tick = ({ yPosition, height, isBold, shouldShowLabel, label }) => {
const top = yPosition - height / 2 - 4
const top = yPosition - height / 2
const containerStyle = [styles.container, { flexBasis: height, height, top }]
const textStyle = isBold ? styles.textBold : styles.textNormal
if (!shouldShowLabel) return null
return (
<View style={containerStyle}>
<AppText style={textStyle}>{label}</AppText>
<AppText style={textStyle}>{shouldShowLabel && label}</AppText>
</View>
)
}
@@ -38,7 +36,6 @@ const styles = StyleSheet.create({
position: 'absolute',
right: 0,
width: CHART_TICK_WIDTH,
minHeight: Sizes.base + 2,
},
textBold: {
fontSize: Sizes.base,
@@ -6,17 +6,16 @@ import AppText from '../common/app-text'
import CloseIcon from '../common/close-icon'
import { Containers, Spacing } from '../../styles'
import { useTranslation } from 'react-i18next'
import { chart } from '../../i18n/en/labels'
const image = require('../../assets/swipe.png')
const Tutorial = ({ onClose }) => {
const { t } = useTranslation()
return (
<View style={styles.container}>
<Image resizeMode="contain" source={image} style={styles.image} />
<View style={styles.textContainer}>
<AppText>{t('chart.tutorial')}</AppText>
<AppText>{chart.tutorial}</AppText>
</View>
<CloseIcon onClose={onClose} />
</View>
+5 -12
View File
@@ -7,16 +7,13 @@ import Asterisk from '../common/Asterisk'
import { Colors, Spacing } from '../../styles'
const Footnote = ({ children, colorLabel }) => {
const Footnote = ({ children }) => {
if (!children) return false
return (
<View style={styles.container}>
<Asterisk />
<AppText
linkStyle={styles.link}
style={{ ...styles.text, color: Colors[colorLabel] }}
>
<AppText linkStyle={styles.link} style={styles.text}>
{children}
</AppText>
</View>
@@ -25,11 +22,6 @@ const Footnote = ({ children, colorLabel }) => {
Footnote.propTypes = {
children: PropTypes.node,
colorLabel: PropTypes.string,
}
Footnote.defaultProps = {
colorLabel: 'greyDark',
}
const styles = StyleSheet.create({
@@ -37,13 +29,14 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignContent: 'flex-start',
marginBottom: Spacing.tiny,
marginTop: Spacing.base,
marginTop: Spacing.small,
},
link: {
color: 'white',
},
text: {
paddingLeft: Spacing.small,
color: Colors.greyLight,
paddingLeft: Spacing.base,
},
})
+13 -48
View File
@@ -1,33 +1,20 @@
import React from 'react'
import {
Dimensions,
Modal,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import { Modal, StyleSheet, TouchableOpacity } from 'react-native'
import PropTypes from 'prop-types'
import CloseIcon from './close-icon'
import { Sizes, Spacing } from '../../styles'
const AppModal = ({ children, onClose }) => (
<Modal
animationType="fade"
onRequestClose={onClose}
transparent={true}
visible={true}
>
<TouchableOpacity onPress={onClose} style={styles.blackBackground} />
<View style={styles.modalWindow}>
<View style={styles.headerContainer}>
<CloseIcon onClose={onClose} />
</View>
const AppModal = ({ children, onClose }) => {
return (
<Modal
animationType="fade"
onRequestClose={onClose}
transparent={true}
visible={true}
>
<TouchableOpacity onPress={onClose} style={styles.blackBackground} />
{children}
</View>
</Modal>
)
</Modal>
)
}
AppModal.propTypes = {
children: PropTypes.node,
@@ -40,28 +27,6 @@ const styles = StyleSheet.create({
flex: 1,
opacity: 0.5,
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
paddingTop: Spacing.base,
paddingHorizontal: Spacing.base,
position: 'absolute',
width: '100%',
zIndex: 3, // works on ios
elevation: 3, // works on android
},
modalWindow: {
alignSelf: 'center',
backgroundColor: 'white',
borderRadius: 10,
marginTop: Sizes.huge * 2,
paddingVertical: Spacing.large * 2,
position: 'absolute',
maxHeight: Dimensions.get('window').height * 0.7,
zIndex: 2, // works on ios
elevation: 2, // works on android
minWidth: '80%',
},
})
export default AppModal
+4 -17
View File
@@ -1,25 +1,18 @@
import React from 'react'
import { Platform, StyleSheet, Switch, View } from 'react-native'
import { StyleSheet, Switch, View } from 'react-native'
import PropTypes from 'prop-types'
import AppText from './app-text'
import { Colors, Containers, Spacing } from '../../styles'
import { Containers } from '../../styles'
const AppSwitch = ({ onToggle, text, value, disabled }) => {
const trackColor = { true: Colors.turquoiseDark }
const AppSwitch = ({ onToggle, text, value }) => {
return (
<View style={styles.container}>
<View style={styles.textContainer}>
<AppText>{text}</AppText>
</View>
<Switch
onValueChange={onToggle}
style={styles.switch}
value={value}
trackColor={trackColor}
disabled={disabled}
/>
<Switch onValueChange={onToggle} style={styles.switch} value={value} />
</View>
)
}
@@ -28,20 +21,14 @@ AppSwitch.propTypes = {
onToggle: PropTypes.func.isRequired,
text: PropTypes.string,
value: PropTypes.bool,
disabled: PropTypes.bool,
}
const styles = StyleSheet.create({
container: {
...Containers.rowContainer,
marginTop: Spacing.tiny,
},
switch: {
flex: 1,
transform:
Platform.OS === 'ios'
? [{ scaleX: 0.8 }, { scaleY: 0.8 }]
: [{ scaleX: 1 }, { scaleY: 1 }],
},
textContainer: {
flex: 4,
+1 -8
View File
@@ -6,14 +6,13 @@ import AppText from './app-text'
import { Colors, Containers, Spacing, Typography } from '../../styles'
const Segment = ({ children, last, title, subheader }) => {
const Segment = ({ children, last, title }) => {
const containerStyle = last ? styles.containerLast : styles.container
const commonStyle = Object.assign({}, containerStyle)
return (
<View style={commonStyle}>
{title && <AppText style={styles.title}>{title}</AppText>}
{subheader && <AppText style={styles.subheader}>{subheader}</AppText>}
{children}
</View>
)
@@ -24,7 +23,6 @@ Segment.propTypes = {
last: PropTypes.bool,
style: PropTypes.object,
title: PropTypes.string,
subheader: PropTypes.string,
}
const styles = StyleSheet.create({
@@ -41,11 +39,6 @@ const styles = StyleSheet.create({
title: {
...Typography.subtitle,
},
subheader: {
...Typography.subtitle,
fontWeight: 'bold',
marginBottom: Spacing.zero,
},
})
export default Segment
@@ -1,63 +0,0 @@
import React from 'react'
import { Platform, StyleSheet, Switch, View } from 'react-native'
import PropTypes from 'prop-types'
import AppText from './app-text'
import DripIcon from '../../assets/drip-icons'
import { Colors, Containers, Sizes, Spacing } from '../../styles'
const TrackingCategorySwitch = ({ onToggle, symptom, text, value }) => {
const trackColor = { true: Colors.turquoiseDark }
const iconColor = value ? Colors.iconColors[symptom].color : Colors.grey
return (
<View style={styles.container}>
<View style={styles.iconContainer}>
<DripIcon
color={iconColor}
name={`drip-icon-${symptom}`}
size={Sizes.title}
/>
</View>
<View style={styles.textContainer}>
<AppText>{text}</AppText>
</View>
<Switch
onValueChange={onToggle}
style={styles.appSwitch}
value={value}
trackColor={trackColor}
/>
</View>
)
}
TrackingCategorySwitch.propTypes = {
onToggle: PropTypes.func.isRequired,
symptom: PropTypes.string,
text: PropTypes.string,
value: PropTypes.bool,
}
const styles = StyleSheet.create({
container: {
...Containers.rowContainer,
marginVertical: Spacing.tiny,
},
iconContainer: {
marginRight: Spacing.tiny,
flex: 1,
},
textContainer: {
flex: 5,
},
appSwitch: {
flex: 2,
transform:
Platform.OS === 'ios'
? [{ scaleX: 0.8 }, { scaleY: 0.8 }]
: [{ scaleX: 1 }, { scaleY: 1 }],
},
})
export default TrackingCategorySwitch
+5 -37
View File
@@ -9,20 +9,13 @@ import SymptomPageTitle from './symptom-page-title'
import { getCycleDay } from '../../db'
import { getData, nextDate, prevDate } from '../helpers/cycle-day'
import {
desireTrackingCategoryObservable,
moodTrackingCategoryObservable,
noteTrackingCategoryObservable,
painTrackingCategoryObservable,
sexTrackingCategoryObservable,
temperatureTrackingCategoryObservable,
mucusTrackingCategoryObservable,
cervixTrackingCategoryObservable,
} from '../../local-storage'
import { useDate } from '../../hooks/useDate'
import { Spacing } from '../../styles'
import { SYMPTOMS } from '../../config'
const CycleDayOverView = ({ date, setDate, isTemperatureEditView }) => {
const CycleDayOverView = ({ isTemperatureEditView }) => {
const { date, setDate } = useDate()
const cycleDay = getCycleDay(date)
const [editedSymptom, setEditedSymptom] = useState(
@@ -37,29 +30,6 @@ const CycleDayOverView = ({ date, setDate, isTemperatureEditView }) => {
setDate(prevDate(date))
}
const allEnabledSymptoms = SYMPTOMS.map((symptom) => {
if (symptom === 'temperature') {
return temperatureTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'sex') {
return sexTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'mucus') {
return mucusTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'cervix') {
return cervixTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'desire') {
return desireTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'pain') {
return painTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'mood') {
return moodTrackingCategoryObservable.value ? symptom : null
} else if (symptom === 'note') {
return noteTrackingCategoryObservable.value ? symptom : null
} else {
return symptom
}
})
const cleanSymptoms = allEnabledSymptoms.filter(Boolean)
return (
<AppPage>
<SymptomPageTitle
@@ -68,7 +38,7 @@ const CycleDayOverView = ({ date, setDate, isTemperatureEditView }) => {
onPrevCycleDay={showPrevCycleDay}
/>
<View style={styles.container}>
{cleanSymptoms.map((symptom) => {
{SYMPTOMS.map((symptom) => {
const symptomData =
cycleDay && cycleDay[symptom] ? cycleDay[symptom] : null
@@ -91,8 +61,6 @@ const CycleDayOverView = ({ date, setDate, isTemperatureEditView }) => {
CycleDayOverView.propTypes = {
cycleDay: PropTypes.object,
date: PropTypes.string,
setDate: PropTypes.func,
isTemperatureEditView: PropTypes.bool,
}
+6 -62
View File
@@ -1,61 +1,22 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native'
import { StyleSheet, TouchableOpacity, View } from 'react-native'
import AppText from '../common/app-text'
import { Colors, Containers } from '../../styles'
import labels from '../../i18n/en/settings'
export default function SelectTabGroup({
activeButton,
buttons,
onSelect,
disabled,
}) {
// TODO https://gitlab.com/bloodyhealth/drip/-/issues/707
const oneTimeTransformIntoNumber =
typeof activeButton === 'boolean' && Number(activeButton)
const isSecondarySymptomSwitch =
buttons[0]['label'] === labels.secondarySymptom.mucus
// Disable is only used for secondarySymptom in customization, if more come up maybe consider more tidy solution
const showDisabledAlert = (label) => {
if (
label === labels.secondarySymptom.cervix ||
label === labels.secondarySymptom.mucus
) {
Alert.alert(
labels.secondarySymptom.disabled.title,
labels.secondarySymptom.disabled.message
)
}
}
export default function SelectTabGroup({ activeButton, buttons, onSelect }) {
return (
<View style={styles.container}>
{buttons.map(({ label, value }, i) => {
const isActive =
value === activeButton || value === oneTimeTransformIntoNumber
const boxStyle = [
styles.box,
isActive && styles.boxActive,
isSecondarySymptomSwitch && styles.purpleBox,
isSecondarySymptomSwitch && isActive && styles.activePurpleBox,
disabled && styles.disabledBox,
]
const textStyle = [
styles.text,
isSecondarySymptomSwitch && styles.purpleText,
isActive && styles.textActive,
disabled && styles.greyText,
]
const isActive = value === activeButton
const boxStyle = [styles.box, isActive && styles.boxActive]
const textStyle = [styles.text, isActive && styles.textActive]
return (
<TouchableOpacity
onPress={() =>
!disabled ? onSelect(value) : showDisabledAlert(label)
}
onPress={() => onSelect(value)}
key={i}
style={boxStyle}
>
@@ -71,7 +32,6 @@ SelectTabGroup.propTypes = {
activeButton: PropTypes.number,
buttons: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
disabled: PropTypes.bool,
}
const styles = StyleSheet.create({
@@ -90,20 +50,4 @@ const styles = StyleSheet.create({
textActive: {
color: 'white',
},
purpleBox: {
borderColor: Colors.purple,
},
activePurpleBox: {
backgroundColor: Colors.purple,
},
purpleText: {
color: Colors.purple,
},
greyText: {
color: Colors.grey,
},
disabledBox: {
borderColor: Colors.grey,
backgroundColor: Colors.turquoiseLight,
},
})
+4 -3
View File
@@ -10,7 +10,7 @@ import SymptomEditView from './symptom-edit-view'
import { isDateInFuture } from '../helpers/cycle-day'
import { Colors, Sizes, Spacing } from '../../styles'
import { useTranslation } from 'react-i18next'
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
const SymptomBox = ({
date,
@@ -20,7 +20,6 @@ const SymptomBox = ({
editedSymptom,
setEditedSymptom,
}) => {
const { t } = useTranslation(null, { keyPrefix: 'symptoms' })
const isSymptomEdited = editedSymptom === symptom
const isSymptomDisabled = isDateInFuture(date) && symptom !== 'note'
const isExcluded = symptomData !== null ? symptomData.exclude : false
@@ -62,7 +61,9 @@ const SymptomBox = ({
size={Sizes.icon}
/>
<View style={styles.textContainer}>
<AppText style={symptomNameStyle}>{t(symptom)}</AppText>
<AppText style={symptomNameStyle}>
{symptomTitles[symptom].toLowerCase()}
</AppText>
{symptomDataToDisplay && (
<AppText style={textStyle} numberOfLines={4}>
{symptomDataToDisplay}
+108 -86
View File
@@ -1,12 +1,13 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { ScrollView, StyleSheet, View } from 'react-native'
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'
import AppModal from '../common/app-modal'
import AppSwitch from '../common/app-switch'
import AppText from '../common/app-text'
import AppTextInput from '../common/app-text-input'
import Button from '../common/button'
import CloseIcon from '../common/close-icon'
import Segment from '../common/segment'
import SelectBoxGroup from './select-box-group'
import SelectTabGroup from './select-tab-group'
@@ -15,7 +16,6 @@ import Temperature from './temperature'
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
import { showToast } from '../helpers/general'
import { fertilityTrackingObservable } from '../../local-storage'
import { shared as sharedLabels } from '../../i18n/en/labels'
import info from '../../i18n/en/symptom-info'
import { Colors, Containers, Sizes, Spacing } from '../../styles'
@@ -26,7 +26,6 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
const [shouldShowInfo, setShouldShowInfo] = useState(false)
const getParsedData = () => JSON.parse(JSON.stringify(data))
const onPressLearnMore = () => setShouldShowInfo(!shouldShowInfo)
const isFertilityTrackingEnabled = fertilityTrackingObservable.value
const onEditNote = (note) => {
const parsedData = getParsedData()
@@ -104,7 +103,8 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
const onSelectTab = (group, value) => {
const parsedData = getParsedData()
parsedData[group.key] = parsedData[group.key] !== value ? value : null
Object.assign(parsedData, { [group.key]: value })
setData(parsedData)
}
const iconName = shouldShowInfo ? 'chevron-up' : 'chevron-down'
@@ -119,60 +119,61 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
return (
<AppModal onClose={onSave}>
<ScrollView
contentContainerStyle={styles.modalContainer}
keyboardDismissMode="on-drag"
>
{symptom === 'temperature' && (
<Temperature
date={date}
data={data}
save={(value, field) => onSaveTemperature(value, field)}
/>
)}
{shouldShow(symptomConfig.selectTabGroups) &&
symtomPage[symptom].selectTabGroups.map((group) => {
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectTabGroup
activeButton={data[group.key]}
buttons={group.options}
onSelect={(value) => onSelectTab(group, value)}
/>
</Segment>
)
})}
{shouldShow(symptomConfig.selectBoxGroups) &&
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) => onSelectBox(value)}
optionsState={data}
/>
{isOtherSelected && (
<AppTextInput
{...inputProps}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={(value) => onSelectBoxNote(value)}
<View style={styles.modalWindow}>
<View style={styles.headerContainer}>
<CloseIcon onClose={onSave} />
</View>
<ScrollView
contentContainerStyle={styles.modalContainer}
keyboardDismissMode="on-drag"
>
{symptom === 'temperature' && (
<Temperature
date={date}
data={data}
save={(value, field) => onSaveTemperature(value, field)}
/>
)}
{shouldShow(symptomConfig.selectTabGroups) &&
symtomPage[symptom].selectTabGroups.map((group) => {
return (
<Segment key={group.key} style={styles.segmentBorder}>
<AppText style={styles.title}>{group.title}</AppText>
<SelectTabGroup
activeButton={data[group.key]}
buttons={group.options}
onSelect={(value) => onSelectTab(group, value)}
/>
)}
</Segment>
)
})}
{/* show exclude AppSwitch for bleeding, mucus, cervix, temperature */}
{/* but if fertility is off only for bleeding */}
{shouldShow(symptomConfig.excludeText) &&
(symptom === 'bleeding' || isFertilityTrackingEnabled) && (
</Segment>
)
})}
{shouldShow(symptomConfig.selectBoxGroups) &&
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) => onSelectBox(value)}
optionsState={data}
/>
{isOtherSelected && (
<AppTextInput
{...inputProps}
placeholder={sharedLabels.enter}
value={data.note}
onChangeText={(value) => onSelectBoxNote(value)}
/>
)}
</Segment>
)
})}
{shouldShow(symptomConfig.excludeText) && (
<Segment style={styles.segmentBorder}>
<AppSwitch
onToggle={onExcludeToggle}
@@ -181,35 +182,36 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
/>
</Segment>
)}
{shouldShow(symptomConfig.note) && (
<Segment style={styles.segmentBorder}>
<AppText>{symtomPage[symptom].note}</AppText>
<AppTextInput
{...inputProps}
onChangeText={onEditNote}
placeholder={sharedLabels.enter}
testID="noteInput"
value={noteText !== null ? noteText : ''}
/>
</Segment>
)}
<View style={styles.buttonsContainer}>
<Button iconName={iconName} isSmall onPress={onPressLearnMore}>
{sharedLabels.learnMore}
</Button>
<Button isSmall onPress={onRemove}>
{sharedLabels.remove}
</Button>
<Button isCTA isSmall onPress={onSave}>
{sharedLabels.save}
</Button>
</View>
{shouldShowInfo && (
<Segment last style={styles.segmentBorder}>
<AppText>{info[symptom].text}</AppText>
</Segment>
)}
</ScrollView>
{shouldShow(symptomConfig.note) && (
<Segment style={styles.segmentBorder}>
<AppText>{symtomPage[symptom].note}</AppText>
<AppTextInput
{...inputProps}
onChangeText={onEditNote}
placeholder={sharedLabels.enter}
testID="noteInput"
value={noteText !== null ? noteText : ''}
/>
</Segment>
)}
<View style={styles.buttonsContainer}>
<Button iconName={iconName} isSmall onPress={onPressLearnMore}>
{sharedLabels.learnMore}
</Button>
<Button isSmall onPress={onRemove}>
{sharedLabels.remove}
</Button>
<Button isCTA isSmall onPress={onSave}>
{sharedLabels.save}
</Button>
</View>
{shouldShowInfo && (
<Segment last style={styles.segmentBorder}>
<AppText>{info[symptom].text}</AppText>
</Segment>
)}
</ScrollView>
</View>
</AppModal>
)
}
@@ -227,12 +229,32 @@ const styles = StyleSheet.create({
paddingHorizontal: Spacing.base,
paddingBottom: Spacing.base,
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
paddingTop: Spacing.base,
paddingHorizontal: Spacing.base,
position: 'absolute',
width: '100%',
zIndex: 3, // works on ios
elevation: 3, // works on android
},
input: {
height: Sizes.base * 5,
},
modalContainer: {
paddingHorizontal: Spacing.base,
},
modalWindow: {
alignSelf: 'center',
backgroundColor: 'white',
borderRadius: 10,
marginTop: Sizes.huge * 2,
paddingTop: Spacing.large * 2,
position: 'absolute',
minHeight: '40%',
maxHeight: Dimensions.get('window').height * 0.7,
},
segmentBorder: {
borderBottomColor: Colors.greyLight,
},
-1
View File
@@ -108,7 +108,6 @@ const styles = StyleSheet.create({
hint: {
fontStyle: 'italic',
fontSize: Sizes.small,
color: Colors.orange,
},
hintContainer: {
marginVertical: Spacing.tiny,
+21 -24
View File
@@ -7,28 +7,28 @@ import {
View,
} from 'react-native'
import PropTypes from 'prop-types'
import AppText from '../common/app-text'
import AppIcon from '../common/app-icon'
import CloseIcon from '../common/close-icon'
import MenuItem from './menu-item'
import { Colors, Sizes, Typography } from '../../styles'
import { Colors, Sizes } from '../../styles'
import settingsLabels from '../../i18n/en/settings'
import { HIT_SLOP } from '../../config'
import { useTranslation } from 'react-i18next'
const { menuItems } = settingsLabels
const settingsMenuItems = [
{ labelKey: 'settings', componentName: 'SettingsMenu' },
{ labelKey: 'about', componentName: 'About' },
{ labelKey: 'license', componentName: 'License' },
{ labelKey: 'privacyPolicy', componentName: 'PrivacyPolicy' },
{ name: menuItems.settings, component: 'SettingsMenu' },
{ name: menuItems.about, component: 'About' },
{ name: menuItems.license, component: 'License' },
{ name: menuItems.privacyPolicy, component: 'PrivacyPolicy' },
]
const HamburgerMenu = ({ navigate }) => {
const [isOpen, setIsOpen] = useState(false)
const closeMenu = () => setIsOpen(false)
const { t } = useTranslation(null, { keyPrefix: 'hamburgerMenu.menuMain' })
if (!isOpen)
return (
<TouchableOpacity onPress={() => setIsOpen(true)} hitSlop={HIT_SLOP}>
@@ -36,25 +36,23 @@ const HamburgerMenu = ({ navigate }) => {
</TouchableOpacity>
)
function onPress(componentName) {
closeMenu()
navigate(componentName)
}
return (
<Modal animationType="fade" onRequestClose={closeMenu} transparent>
<TouchableOpacity onPress={closeMenu} style={styles.blackBackground} />
<Modal animationType="fade" onRequestClose={closeMenu} transparent={true}>
<TouchableOpacity
onPress={closeMenu}
style={styles.blackBackground}
></TouchableOpacity>
<View style={styles.menu}>
<View style={styles.iconContainer}>
<CloseIcon color={'black'} onClose={closeMenu} />
</View>
{settingsMenuItems.map(({ labelKey, componentName }) => (
<TouchableOpacity
onPress={() => onPress(componentName)}
key={labelKey}
>
<AppText style={styles.menuItem}>{t(labelKey)}</AppText>
</TouchableOpacity>
{settingsMenuItems.map((item) => (
<MenuItem
item={item}
key={item.name}
closeMenu={closeMenu}
navigate={navigate}
/>
))}
</View>
</Modal>
@@ -86,5 +84,4 @@ const styles = StyleSheet.create({
position: 'absolute',
width: '60%',
},
menuItem: Typography.subtitle,
})
+35
View File
@@ -0,0 +1,35 @@
import React from 'react'
import { StyleSheet, TouchableOpacity } from 'react-native'
import PropTypes from 'prop-types'
import AppText from '../common/app-text'
import { Typography } from '../../styles'
const MenuItem = ({ item, navigate, closeMenu }) => {
const { component, name } = item
const onPress = () => {
closeMenu()
navigate(component)
}
return (
<TouchableOpacity onPress={onPress}>
<AppText style={styles.text}>{name}</AppText>
</TouchableOpacity>
)
}
MenuItem.propTypes = {
item: PropTypes.object.isRequired,
navigate: PropTypes.func.isRequired,
closeMenu: PropTypes.func.isRequired,
}
const styles = StyleSheet.create({
text: {
...Typography.subtitle,
},
})
export default MenuItem
-2
View File
@@ -2,7 +2,6 @@ import { LocalDate } from '@js-joda/core'
import { verticalScale } from 'react-native-size-matters'
import { Colors, Fonts, Sizes } from '../../styles'
import { periodPredictionObservable } from '../../local-storage'
const { shades } = Colors.iconColors.bleeding
@@ -27,7 +26,6 @@ export const toCalFormat = (bleedingDaysSortedByDate) => {
}
export const predictionToCalFormat = (predictedDays) => {
if (!periodPredictionObservable.value) return {}
if (!predictedDays.length) return {}
const todayDateString = LocalDate.now().toString()
const middleIndex = (predictedDays[0].length - 1) / 2
+2 -7
View File
@@ -1,10 +1,6 @@
import { LocalDate } from '@js-joda/core'
import {
fertilityTrackingObservable,
scaleObservable,
unitObservable,
} from '../../local-storage'
import { scaleObservable, unitObservable } from '../../local-storage'
import { getCycleStatusForDay } from '../../lib/sympto-adapter'
import { getCycleDay, getAmountOfCycleDays } from '../../db'
@@ -274,8 +270,7 @@ export function nfpLines() {
if (dateString < cycle.startDate) updateCurrentCycle(dateString)
if (cycle.noMoreCycles) return ret
const tempShift =
fertilityTrackingObservable.value && cycle.status.temperatureShift
const tempShift = cycle.status.temperatureShift
if (tempShift) {
if (tempShift.firstHighMeasurementDay.date === dateString) {
-4
View File
@@ -34,10 +34,6 @@ export const formatTemperature = (temperature) =>
? temperature
: Number.parseFloat(temperature.toString()).toFixed(2)
//maximum of precision digits after decimal point, but no x.0
export const formatDecimal = (num, precision) =>
+parseFloat(Number.parseFloat(num).toFixed(precision))
export const getPreviousTemperature = (date) => {
const previousTemperature = getPreviousTemperatureForDate(date)
return formatTemperature(previousTemperature)
+3 -6
View File
@@ -6,23 +6,20 @@ import MenuItem from './menu-item'
import { Containers } from '../../styles'
import { pages } from '../pages'
import { useTranslation } from 'react-i18next'
const Menu = ({ currentPage, navigate }) => {
const menuItems = pages.filter((page) => page.isInMenu)
const { t } = useTranslation(null, { keyPrefix: 'bottomMenu' })
return (
<View style={styles.container}>
{menuItems.map(({ icon, labelKey, component }) => {
{menuItems.map(({ icon, label, component }) => {
return (
<MenuItem
isActive={component === currentPage}
onPress={() => navigate(component)}
icon={icon}
key={labelKey}
label={t(labelKey)}
key={label}
label={label}
/>
)
})}
+16 -8
View File
@@ -1,67 +1,75 @@
import settingsViews from './settings'
import settingsLabels from '../i18n/en/settings'
const labels = settingsLabels.menuItems
export const pages = [
{
component: 'Home',
icon: 'home',
label: 'Home',
},
{
component: 'Calendar',
icon: 'calendar',
isInMenu: true,
labelKey: 'calendar',
label: 'Calendar',
parent: 'Home',
},
{
component: 'Chart',
icon: 'chart',
isInMenu: true,
labelKey: 'chart',
label: 'Chart',
parent: 'Home',
},
{
component: 'Stats',
icon: 'statistics',
isInMenu: true,
labelKey: 'stats',
label: 'Stats',
parent: 'Home',
},
{
children: Object.keys(settingsViews),
component: 'SettingsMenu',
icon: 'settings',
label: 'Settings',
parent: 'Home',
},
{
component: 'Reminders',
label: labels.reminders.name,
parent: 'SettingsMenu',
},
{
component: 'Customization',
component: 'NfpSettings',
label: labels.nfpSettings.name,
parent: 'SettingsMenu',
},
{
component: 'DataManagement',
parent: 'SettingsMenu',
},
{
component: 'Info',
label: labels.dataManagement.name,
parent: 'SettingsMenu',
},
{
component: 'Password',
label: labels.password.name,
parent: 'SettingsMenu',
},
{
component: 'About',
label: 'About',
parent: 'SettingsMenu',
},
{
component: 'License',
label: 'License',
parent: 'SettingsMenu',
},
{
component: 'PrivacyPolicy',
label: 'PrivacyPolicy',
parent: 'SettingsMenu',
},
{
+12 -6
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { Alert, KeyboardAvoidingView, StyleSheet, View } from 'react-native'
import { SHA512 } from 'jshashes'
import nodejs from 'nodejs-mobile-react-native'
import AppPage from './common/app-page'
import AppTextInput from './common/app-text-input'
@@ -9,7 +9,7 @@ import Button from './common/button'
import Header from './header'
import { saveEncryptionFlag } from '../local-storage'
import { deleteDbAndOpenNew, openDb } from '../db'
import { requestHash, deleteDbAndOpenNew, openDb } from '../db'
import { passwordPrompt as labels, shared } from '../i18n/en/labels'
import { Containers, Spacing } from '../styles'
@@ -17,10 +17,9 @@ const cancelButton = { text: shared.cancel, style: 'cancel' }
const PasswordPrompt = ({ enableShowApp }) => {
const [password, setPassword] = useState(null)
const unlockApp = () => requestHash('check-pw', password)
const isPasswordEntered = Boolean(password)
const unlockApp = async () => {
const hash = new SHA512().hex(password)
const passHashToDb = async (hash) => {
const connected = await openDb(hash)
if (!connected) {
@@ -32,9 +31,16 @@ const PasswordPrompt = ({ enableShowApp }) => {
])
return
}
enableShowApp()
}
useEffect(() => {
const listener = nodejs.channel.addListener('check-pw', passHashToDb, this)
return () => listener.remove()
}, [])
const onDeleteDataConfirmation = async () => {
await deleteDbAndOpenNew()
await saveEncryptionFlag(false)
+1 -1
View File
@@ -12,7 +12,7 @@ import links from '../../i18n/en/links'
import { useTranslation } from 'react-i18next'
const AboutSection = () => {
const { t } = useTranslation(null, { keyPrefix: 'hamburgerMenu.about' })
const { t } = useTranslation(null, { keyPrefix: 'settings.about' })
return (
<AppPage title={t('title')}>
-50
View File
@@ -1,50 +0,0 @@
import React from 'react'
import { StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import AppIcon from '../common/app-icon'
import AppPage from '../common/app-page'
import AppText from '../common/app-text'
import Segment from '../common/segment'
import { Colors, Spacing, Typography } from '../../styles'
import labels from '../../i18n/en/settings'
const Info = () => {
const { t } = useTranslation(null, { keyPrefix: 'hamburgerMenu.info' })
return (
<AppPage title={t('title')}>
<Segment last>
<View style={styles.line}>
<AppIcon
color={Colors.purple}
name="info-with-circle"
style={styles.icon}
/>
<AppText style={styles.title}>{labels.preOvu.title}</AppText>
</View>
<AppText>{labels.preOvu.note}</AppText>
</Segment>
</AppPage>
)
}
Info.propTypes = {
children: PropTypes.node,
}
export default Info
const styles = StyleSheet.create({
icon: {
marginRight: Spacing.base,
},
line: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
...Typography.subtitle,
},
})
+2 -10
View File
@@ -1,20 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import AppPage from '../common/app-page'
import AppText from '../common/app-text'
import AppLink from '../common/AppLink'
import Segment from '../common/segment'
import { Spacing } from '../../styles'
const License = ({ children }) => {
const { t } = useTranslation(null, { keyPrefix: 'hamburgerMenu.license' })
const { t } = useTranslation(null, { keyPrefix: 'settings.license' })
const currentYear = new Date().getFullYear()
const link = 'https://www.gnu.org/licenses/gpl-3.0.html'
return (
<AppPage title={t('title')} contentContainerStyle={styles.contentContainer}>
<AppPage title={t('title')}>
<Segment last>
<AppText>{t('text', { currentYear })}</AppText>
<AppLink url={link}>{link}</AppLink>
@@ -28,10 +26,4 @@ License.propTypes = {
children: PropTypes.node,
}
const styles = StyleSheet.create({
contentContainer: {
marginTop: Spacing.large,
},
})
export default License
@@ -1,12 +1,12 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Alert, KeyboardAvoidingView, StyleSheet, View } from 'react-native'
import { SHA512 } from 'jshashes'
import nodejs from 'nodejs-mobile-react-native'
import AppTextInput from '../../common/app-text-input'
import Button from '../../common/button'
import { openDb } from '../../../db'
import { requestHash, openDb } from '../../../db'
import { Containers } from '../../../styles'
import settings from '../../../i18n/en/settings'
import { shared } from '../../../i18n/en/labels'
@@ -14,8 +14,7 @@ import { shared } from '../../../i18n/en/labels'
const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
const [password, setPassword] = useState(null)
const checkPassword = async () => {
const hash = new SHA512().hex(password)
const checkPassword = async (hash) => {
try {
await openDb(hash)
onSuccess()
@@ -24,6 +23,15 @@ const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
}
}
useEffect(() => {
const listener = nodejs.channel.addListener(
'password-check',
checkPassword,
this
)
return () => listener.remove()
}, [])
const onIncorrectPassword = () => {
Alert.alert(shared.incorrectPassword, shared.incorrectPasswordMessage, [
{
@@ -37,6 +45,10 @@ const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
])
}
const initPasswordCheck = () => {
requestHash('password-check', password)
}
const labels = settings.passwordSettings
const isPassword = password !== null
@@ -53,7 +65,7 @@ const ConfirmWithPassword = ({ onSuccess, onCancel }) => {
<Button
disabled={!isPassword}
isCTA={isPassword}
onPress={checkPassword}
onPress={initPasswordCheck}
>
{shared.confirmToProceed}
</Button>
-351
View File
@@ -1,351 +0,0 @@
import React, { useEffect, useState } from 'react'
import { Alert, Pressable, StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next'
import AppIcon from '../../common/app-icon'
import AppPage from '../../common/app-page'
import AppSwitch from '../../common/app-switch'
import AppText from '../../common/app-text'
import { Colors, Spacing, Typography } from '../../../styles'
import TemperatureSlider from './temperature-slider'
import Segment from '../../common/segment'
import TrackingCategorySwitch from '../../common/tracking-category-switch'
import SelectTabGroup from '../../cycle-day/select-tab-group'
import {
desireTrackingCategoryObservable,
fertilityTrackingObservable,
moodTrackingCategoryObservable,
noteTrackingCategoryObservable,
painTrackingCategoryObservable,
sexTrackingCategoryObservable,
temperatureTrackingCategoryObservable,
mucusTrackingCategoryObservable,
cervixTrackingCategoryObservable,
periodPredictionObservable,
useCervixAsSecondarySymptomObservable,
saveDesireTrackingCategory,
saveFertilityTrackingEnabled,
saveMoodTrackingCategory,
saveNoteTrackingCategory,
savePainTrackingCategory,
saveMucusTrackingCategory,
saveCervixTrackingCategory,
savePeriodPrediction,
saveSexTrackingCategory,
saveTemperatureTrackingCategory,
saveUseCervixAsSecondarySymptom,
} from '../../../local-storage'
import labels from '../../../i18n/en/settings'
import { SYMPTOMS } from '../../../config'
const Settings = () => {
const { t } = useTranslation(null, { keyPrefix: 'symptoms' })
const [useCervixAsSecondarySymptom, setUseCervixAsSecondarySymptom] =
useState(useCervixAsSecondarySymptomObservable.value)
const [isPeriodPredictionEnabled, setPeriodPrediction] = useState(
periodPredictionObservable.value
)
const [isTemperatureTrackingCategoryEnabled, setTemperatureTrackingCategory] =
useState(temperatureTrackingCategoryObservable.value)
const [isMucusTrackingCategoryEnabled, setMucusTrackingCategory] = useState(
mucusTrackingCategoryObservable.value
)
const [isCervixTrackingCategoryEnabled, setCervixTrackingCategory] = useState(
cervixTrackingCategoryObservable.value
)
const [isSexTrackingCategoryEnabled, setSexTrackingCategory] = useState(
sexTrackingCategoryObservable.value
)
const [isDesireTrackingCategoryEnabled, setDesireTrackingCategory] = useState(
desireTrackingCategoryObservable.value
)
const [isPainTrackingCategoryEnabled, setPainTrackingCategory] = useState(
painTrackingCategoryObservable.value
)
const [isMoodTrackingCategoryEnabled, setMoodTrackingCategory] = useState(
moodTrackingCategoryObservable.value
)
const [isNoteTrackingCategoryEnabled, setNoteTrackingCategory] = useState(
noteTrackingCategoryObservable.value
)
const [isFertilityTrackingEnabled, setFertilityTrackingEnabled] = useState(
fertilityTrackingObservable.value
)
const fertilityTrackingToggle = (value) => {
setFertilityTrackingEnabled(value)
saveFertilityTrackingEnabled(value)
}
const temperatureTrackingCategoryToggle = (value) => {
setTemperatureTrackingCategory(value)
saveTemperatureTrackingCategory(value)
if (!value) {
setFertilityTrackingEnabled(false)
saveFertilityTrackingEnabled(false)
}
}
const mucusTrackingCategoryToggle = (value) => {
manageSecondarySymptom(cervixTrackingCategoryObservable.value, value)
}
const cervixTrackingCategoryToggle = (value) => {
manageSecondarySymptom(value, mucusTrackingCategoryObservable.value)
}
const sexTrackingCategoryToggle = (value) => {
setSexTrackingCategory(value)
saveSexTrackingCategory(value)
}
const desireTrackingCategoryToggle = (value) => {
setDesireTrackingCategory(value)
saveDesireTrackingCategory(value)
}
const painTrackingCategoryToggle = (value) => {
setPainTrackingCategory(value)
savePainTrackingCategory(value)
}
const moodTrackingCategoryToggle = (value) => {
setMoodTrackingCategory(value)
saveMoodTrackingCategory(value)
}
const noteTrackingCategoryToggle = (value) => {
setNoteTrackingCategory(value)
saveNoteTrackingCategory(value)
}
const onPeriodPredictionToggle = (value) => {
setPeriodPrediction(value)
savePeriodPrediction(value)
}
const fertilityTrackingText = isFertilityTrackingEnabled
? labels.fertilityTracking.on
: labels.fertilityTracking.off
const periodPredictionText = isPeriodPredictionEnabled
? labels.periodPrediction.on
: labels.periodPrediction.off
const secondarySymptomButtons = [
{
label: labels.secondarySymptom.mucus,
value: 0,
},
{
label: labels.secondarySymptom.cervix,
value: 1,
},
]
const onSelectTab = (value) => {
if (isMucusTrackingCategoryEnabled && isCervixTrackingCategoryEnabled) {
setUseCervixAsSecondarySymptom(value)
saveUseCervixAsSecondarySymptom(value)
} else {
secondarySymptomDisabledPrompt()
}
}
// is needed so secondary symptom is set correct on load
useEffect(() => {
manageSecondarySymptom(
cervixTrackingCategoryObservable.value,
mucusTrackingCategoryObservable.value
)
}, [])
const manageSecondarySymptom = (cervix, mucus) => {
if (!cervix && mucus) {
setUseCervixAsSecondarySymptom(0)
saveUseCervixAsSecondarySymptom(0)
} else if (cervix && !mucus) {
setUseCervixAsSecondarySymptom(1)
saveUseCervixAsSecondarySymptom(1)
} else if (!cervix && !mucus) {
setFertilityTrackingEnabled(false)
saveFertilityTrackingEnabled(false)
}
setMucusTrackingCategory(mucus)
saveMucusTrackingCategory(mucus)
setCervixTrackingCategory(cervix)
saveCervixTrackingCategory(cervix)
}
const secondarySymptomDisabledPrompt = () => {
if (!isFertilityTrackingEnabled) {
Alert.alert(
labels.secondarySymptom.disabled.title,
labels.secondarySymptom.disabled.message
)
} else if (
!isMucusTrackingCategoryEnabled == isCervixTrackingCategoryEnabled
) {
Alert.alert(
labels.secondarySymptom.disabled.title,
labels.secondarySymptom.disabled.noSecondaryEnabled
)
}
}
const manageFertilityFeature =
isTemperatureTrackingCategoryEnabled &&
(isMucusTrackingCategoryEnabled || isCervixTrackingCategoryEnabled)
const cervixText = useCervixAsSecondarySymptom
? labels.secondarySymptom.cervixModeOn
: labels.secondarySymptom.cervixModeOff
const sliderDisabledPrompt = () => {
if (!isTemperatureTrackingCategoryEnabled) {
Alert.alert(labels.tempScale.disabled, labels.tempScale.disabledMessage)
}
}
const fertilityDisabledPrompt = () => {
if (!manageFertilityFeature) {
Alert.alert(
labels.fertilityTracking.disabledTitle,
labels.fertilityTracking.disabled
)
}
}
return (
<AppPage title={labels.customization.title}>
<Segment title={labels.customization.trackingCategories}>
<TrackingCategorySwitch
onToggle={temperatureTrackingCategoryToggle}
text={t(SYMPTOMS[1])}
value={isTemperatureTrackingCategoryEnabled}
symptom={SYMPTOMS[1]}
/>
<TrackingCategorySwitch
onToggle={(enabled) => {
mucusTrackingCategoryToggle(enabled)
}}
text={t(SYMPTOMS[2])}
value={isMucusTrackingCategoryEnabled}
symptom={SYMPTOMS[2]}
/>
<TrackingCategorySwitch
onToggle={(enabled) => {
cervixTrackingCategoryToggle(enabled)
}}
text={t(SYMPTOMS[3])}
value={isCervixTrackingCategoryEnabled}
symptom={SYMPTOMS[3]}
/>
<TrackingCategorySwitch
onToggle={sexTrackingCategoryToggle}
text={t(SYMPTOMS[4])}
value={isSexTrackingCategoryEnabled}
symptom={SYMPTOMS[4]}
/>
<TrackingCategorySwitch
onToggle={desireTrackingCategoryToggle}
text={t(SYMPTOMS[5])}
value={isDesireTrackingCategoryEnabled}
symptom={SYMPTOMS[5]}
/>
<TrackingCategorySwitch
onToggle={painTrackingCategoryToggle}
text={t(SYMPTOMS[6])}
value={isPainTrackingCategoryEnabled}
symptom={SYMPTOMS[6]}
/>
<TrackingCategorySwitch
onToggle={moodTrackingCategoryToggle}
text={t(SYMPTOMS[7])}
value={isMoodTrackingCategoryEnabled}
symptom={SYMPTOMS[7]}
/>
<TrackingCategorySwitch
onToggle={noteTrackingCategoryToggle}
text={t(SYMPTOMS[8])}
value={isNoteTrackingCategoryEnabled}
symptom={SYMPTOMS[8]}
/>
</Segment>
<Pressable onPress={fertilityDisabledPrompt}>
<Segment title={labels.fertilityTracking.title}>
<AppText>{labels.fertilityTracking.message}</AppText>
<AppSwitch
onToggle={fertilityTrackingToggle}
text={fertilityTrackingText}
value={isFertilityTrackingEnabled}
disabled={!manageFertilityFeature}
/>
</Segment>
</Pressable>
<Segment title={labels.periodPrediction.title}>
<AppSwitch
onToggle={onPeriodPredictionToggle}
text={periodPredictionText}
value={isPeriodPredictionEnabled}
/>
</Segment>
<Segment
subheader={labels.customization.subheaderSymptoThermalMethod}
last
></Segment>
<Pressable onPress={sliderDisabledPrompt}>
<Segment title={labels.tempScale.segmentTitle}>
<AppText>{labels.tempScale.segmentExplainer}</AppText>
<TemperatureSlider disabled={!isTemperatureTrackingCategoryEnabled} />
</Segment>
</Pressable>
<Pressable onPress={secondarySymptomDisabledPrompt}>
<Segment title={labels.secondarySymptom.title}>
<AppText>{cervixText}</AppText>
<SelectTabGroup
activeButton={useCervixAsSecondarySymptom}
buttons={secondarySymptomButtons}
onSelect={(value) => onSelectTab(value)}
disabled={!isFertilityTrackingEnabled}
/>
</Segment>
</Pressable>
<Segment last>
<View style={styles.line}>
<AppIcon
color={Colors.purple}
name="info-with-circle"
style={styles.icon}
/>
<AppText style={styles.title}>{labels.preOvu.title}</AppText>
</View>
<AppText>{labels.preOvu.note}</AppText>
</Segment>
</AppPage>
)
}
export default Settings
const styles = StyleSheet.create({
icon: {
marginRight: Spacing.base,
},
line: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
...Typography.subtitle,
},
})
@@ -1,104 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Alert, Platform } from 'react-native'
import DocumentPicker from 'react-native-document-picker'
import rnfs from 'react-native-fs'
import importCsv from '../../../lib/import-export/import-from-csv'
import alertError from '../common/alert-error'
import Segment from '../../common/segment'
import AppText from '../../common/app-text'
import Button from '../../common/button'
import { useTranslation } from 'react-i18next'
export default function ImportData({ resetIsDeletingData, setIsLoading }) {
const { t } = useTranslation(null, {
keyPrefix: 'hamburgerMenu.settings.data.import',
})
async function startImport(shouldDeleteExistingData) {
setIsLoading(true)
await importData(shouldDeleteExistingData)
setIsLoading(false)
}
async function getFileInfo() {
try {
const fileInfo = await DocumentPicker.pickSingle({
type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
})
return fileInfo
} catch (error) {
if (DocumentPicker.isCancel(error)) return // User cancelled the picker, exit any dialogs or menus and move on
showImportErrorAlert(error)
}
}
async function getFileContent() {
const fileInfo = await getFileInfo()
if (!fileInfo) return null
try {
const fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
return fileContent
} catch (err) {
return showImportErrorAlert(t('error.couldNotOpenFile'))
}
}
async function importData(shouldDeleteExistingData) {
const fileContent = await getFileContent()
if (!fileContent) return
try {
await importCsv(fileContent, shouldDeleteExistingData)
Alert.alert(t('success.title'), t('success.message'))
} catch (err) {
showImportErrorAlert(err.message)
}
}
function openImportDialog() {
resetIsDeletingData()
let buttons = [
{
text: t('dialog.cancel'),
style: 'cancel',
onPress: () => {},
},
{
text: t('dialog.replace'),
onPress: () => startImport(false),
},
{
text: t('dialog.delete'),
onPress: () => startImport(true),
},
]
if (Platform.OS === 'android') {
buttons = [buttons[0], buttons[2], buttons[1]]
}
Alert.alert(t('dialog.title'), t('dialog.message'), buttons)
}
function showImportErrorAlert(message) {
const errorMessage = t('error.noDataImported', { message })
alertError(errorMessage)
}
return (
<Segment title={t('button')}>
<AppText>{t('segmentExplainer')}</AppText>
<Button isCTA onPress={openImportDialog}>
{t('button')}
</Button>
</Segment>
)
}
ImportData.propTypes = {
resetIsDeletingData: PropTypes.func.isRequired,
setIsLoading: PropTypes.func.isRequired,
}
@@ -0,0 +1,64 @@
import { Alert } from 'react-native'
import DocumentPicker from 'react-native-document-picker'
import rnfs from 'react-native-fs'
import importCsv from '../../../lib/import-export/import-from-csv'
import { shared as sharedLabels } from '../../../i18n/en/labels'
import labels from '../../../i18n/en/settings'
import alertError from '../common/alert-error'
export function openImportDialog(onImportData) {
Alert.alert(labels.import.title, labels.import.message, [
{
text: sharedLabels.cancel,
style: 'cancel',
onPress: () => {},
},
{
text: labels.import.replaceOption,
onPress: () => onImportData(false),
},
{
text: labels.import.deleteOption,
onPress: () => onImportData(true),
},
])
}
export async function getFileContent() {
let fileInfo
try {
fileInfo = await DocumentPicker.pickSingle({
type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
})
} catch (error) {
if (DocumentPicker.isCancel(error)) {
// User cancelled the picker, exit any dialogs or menus and move on
return
} else {
importError(error)
}
}
let fileContent
try {
fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
} catch (err) {
return importError(labels.import.errors.couldNotOpenFile)
}
return fileContent
}
export async function importData(shouldDeleteExistingData, fileContent) {
try {
await importCsv(fileContent, shouldDeleteExistingData)
Alert.alert(sharedLabels.successTitle, labels.import.success.message)
} catch (err) {
importError(err.message)
}
}
function importError(msg) {
const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
alertError(postFixed)
}
@@ -6,23 +6,40 @@ import AppText from '../../common/app-text'
import Button from '../../common/button'
import Segment from '../../common/segment'
import { openImportDialog, getFileContent, importData } from './import-dialog'
import openShareDialogAndExport from './export-dialog'
import DeleteData from './delete-data'
import labels from '../../../i18n/en/settings'
import ImportData from './ImportData'
import { ACTION_DELETE, ACTION_EXPORT, ACTION_IMPORT } from '../../../config'
const DataManagement = () => {
const [isLoading, setIsLoading] = useState(false)
const [isDeletingData, setIsDeletingData] = useState(false)
const [currentAction, setCurrentAction] = useState(null)
const startImportFlow = async (shouldDeleteExistingData) => {
setIsLoading(true)
const fileContent = await getFileContent()
if (fileContent) {
await importData(shouldDeleteExistingData, fileContent)
}
setIsLoading(false)
}
const startExport = () => {
setIsDeletingData(false)
setCurrentAction(ACTION_EXPORT)
openShareDialogAndExport()
}
const startImport = () => {
setCurrentAction(ACTION_IMPORT)
openImportDialog(startImportFlow)
}
if (isLoading) return <AppLoadingView />
const isDeletingData = currentAction === ACTION_DELETE
return (
<AppPage>
<Segment title={labels.export.button}>
@@ -31,15 +48,17 @@ const DataManagement = () => {
{labels.export.button}
</Button>
</Segment>
<ImportData
resetIsDeletingData={() => setIsDeletingData(false)}
setIsLoading={setIsLoading}
/>
<Segment title={labels.import.button}>
<AppText>{labels.import.segmentExplainer}</AppText>
<Button isCTA onPress={startImport}>
{labels.import.button}
</Button>
</Segment>
<Segment title={labels.deleteSegment.title} last>
<AppText>{labels.deleteSegment.explainer}</AppText>
<DeleteData
isDeletingData={isDeletingData}
onStartDeletion={() => setIsDeletingData(true)}
onStartDeletion={() => setCurrentAction(ACTION_DELETE)}
/>
</Segment>
</AppPage>
+3 -5
View File
@@ -1,7 +1,6 @@
import Reminders from './reminders/reminders'
import Customization from './customization'
import DataManagement from './data-management/DataManagement'
import Info from './Info'
import NfpSettings from './nfp-settings'
import DataManagement from './data-management'
import Password from './password'
import About from './About'
import License from './License'
@@ -9,9 +8,8 @@ import PrivacyPolicy from './privacy-policy'
export default {
Reminders,
Customization,
NfpSettings,
DataManagement,
Info,
Password,
About,
License,
+6 -21
View File
@@ -7,28 +7,20 @@ import AppText from '../common/app-text'
import Segment from '../common/segment'
import { Colors, Containers, Sizes } from '../../styles'
import { useTranslation } from 'react-i18next'
const MenuItem = ({ item, last, navigate }) => {
const { t } = useTranslation(null, {
keyPrefix: 'hamburgerMenu.settings.menuItem',
})
return (
<Segment last={last}>
<TouchableOpacity
style={styles.container}
key={item.label}
onPress={() => navigate(item.componentName)}
key={item.name}
onPress={() => navigate(item.component)}
>
<View style={styles.textContainer}>
<AppText style={styles.title}>{t(`${item.label}.name`)}</AppText>
{!!item.label && <AppText>{t(`${item.label}.text`)}</AppText>}
<View>
<AppText style={styles.title}>{item.name}</AppText>
{item.text.length > 0 && <AppText>{item.text}</AppText>}
</View>
<AppIcon
style={styles.chevronContainer}
name="chevron-right"
color={Colors.orange}
/>
<AppIcon name="chevron-right" color={Colors.orange} />
</TouchableOpacity>
</Segment>
)
@@ -48,13 +40,6 @@ const styles = StyleSheet.create({
color: Colors.purple,
fontSize: Sizes.subtitle,
},
textContainer: {
flex: 5,
},
chevronContainer: {
textAlign: 'right',
flex: 1,
},
})
export default MenuItem
+73
View File
@@ -0,0 +1,73 @@
import React, { useState } from 'react'
import { Platform, StyleSheet, View } from 'react-native'
import AppIcon from '../../common/app-icon'
import AppPage from '../../common/app-page'
import AppSwitch from '../../common/app-switch'
import AppText from '../../common/app-text'
import TemperatureSlider from './temperature-slider'
import Segment from '../../common/segment'
import { useCervixObservable, saveUseCervix } from '../../../local-storage'
import { Colors, Spacing, Typography } from '../../../styles'
import labels from '../../../i18n/en/settings'
const Settings = () => {
const [shouldUseCervix, setShouldUseCervix] = useState(
useCervixObservable.value
)
const onCervixToggle = (value) => {
setShouldUseCervix(value)
saveUseCervix(value)
}
const cervixText = shouldUseCervix
? labels.useCervix.cervixModeOn
: labels.useCervix.cervixModeOff
return (
<AppPage>
<Segment title={labels.useCervix.title}>
<AppSwitch
onToggle={onCervixToggle}
text={cervixText}
value={shouldUseCervix}
/>
</Segment>
{/* for iOS disabled temporarily, TODO https://gitlab.com/bloodyhealth/drip/-/issues/545 */}
{Platform.OS !== 'ios' && (
<Segment title={labels.tempScale.segmentTitle}>
<AppText>{labels.tempScale.segmentExplainer}</AppText>
<TemperatureSlider />
</Segment>
)}
<Segment last>
<View style={styles.line}>
<AppIcon
color={Colors.purple}
name="info-with-circle"
style={styles.icon}
/>
<AppText style={styles.title}>{labels.preOvu.title}</AppText>
</View>
<AppText>{labels.preOvu.note}</AppText>
</Segment>
</AppPage>
)
}
export default Settings
const styles = StyleSheet.create({
icon: {
marginRight: Spacing.base,
},
line: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
...Typography.subtitle,
},
})
@@ -1,6 +1,5 @@
import React, { useState } from 'react'
import { StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import Slider from '@ptomasroos/react-native-multi-slider'
import alertError from '../common/alert-error'
@@ -11,7 +10,7 @@ import { Colors, Sizes } from '../../../styles'
import labels from '../../../i18n/en/settings'
import { TEMP_MIN, TEMP_MAX, TEMP_SLIDER_STEP } from '../../../config'
const TemperatureSlider = ({ disabled }) => {
const TemperatureSlider = () => {
const savedValue = scaleObservable.value
const [minTemperature, setMinTemperature] = useState(savedValue.min)
const [maxTemperature, setMaxTemperature] = useState(savedValue.max)
@@ -26,14 +25,6 @@ const TemperatureSlider = ({ disabled }) => {
}
}
const sliderAccentBackground = disabled
? styles.disabledSliderAccentBackground
: styles.sliderAccentBackground
const sliderBackground = disabled
? styles.disabledSliderBackground
: styles.sliderBackground
return (
<View style={styles.container}>
<Slider
@@ -44,13 +35,11 @@ const TemperatureSlider = ({ disabled }) => {
max={TEMP_MAX}
min={TEMP_MIN}
onValuesChange={onTemperatureSliderChange}
selectedStyle={styles.sliderAccentBackground}
step={TEMP_SLIDER_STEP}
trackStyle={styles.slider}
unselectedStyle={styles.sliderBackground}
values={[minTemperature, maxTemperature]}
enabledOne={!disabled}
enabledTwo={!disabled}
selectedStyle={sliderAccentBackground}
unselectedStyle={sliderBackground}
/>
</View>
)
@@ -58,10 +47,6 @@ const TemperatureSlider = ({ disabled }) => {
export default TemperatureSlider
TemperatureSlider.propTypes = {
disabled: PropTypes.bool,
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
@@ -69,7 +54,6 @@ const styles = StyleSheet.create({
},
marker: {
backgroundColor: Colors.turquoiseDark,
borderRadius: 50,
elevation: 4,
height: Sizes.subtitle,
@@ -82,13 +66,7 @@ const styles = StyleSheet.create({
sliderAccentBackground: {
backgroundColor: Colors.turquoiseDark,
},
disabledSliderAccentBackground: {
backgroundColor: Colors.grey,
},
sliderBackground: {
backgroundColor: Colors.turquoise,
},
disabledSliderBackground: {
backgroundColor: Colors.greyLight,
},
})
@@ -1,24 +1,35 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { KeyboardAvoidingView, StyleSheet } from 'react-native'
import nodejs from 'nodejs-mobile-react-native'
import PropTypes from 'prop-types'
import { SHA512 } from 'jshashes'
import AppText from '../../common/app-text'
import AppTextInput from '../../common/app-text-input'
import Button from '../../common/button'
import { requestHash } from '../../../db'
import { Colors, Spacing } from '../../../styles'
import settings from '../../../i18n/en/settings'
const LISTENER_TYPE = 'create-or-change-pw'
const EnterNewPassword = ({ changeEncryptionAndRestart }) => {
const [password, setPassword] = useState('')
const [passwordConfirmation, setPasswordConfirmation] = useState('')
const [shouldShowErrorMessage, setShouldShowErrorMessage] = useState(false)
useEffect(() => {
const listener = nodejs.channel.addListener(
LISTENER_TYPE,
changeEncryptionAndRestart,
this
)
return () => listener.remove()
}, [])
const savePassword = () => {
if (comparePasswords()) {
const hash = new SHA512().hex(password)
changeEncryptionAndRestart(hash)
requestHash(LISTENER_TYPE, password)
} else {
setShouldShowErrorMessage(true)
}
+1 -3
View File
@@ -9,9 +9,7 @@ import Segment from '../common/segment'
import { Colors, Sizes } from '../../styles'
const PrivacyPolicy = () => {
const { t } = useTranslation(null, {
keyPrefix: 'hamburgerMenu.privacyPolicy',
})
const { t } = useTranslation(null, { keyPrefix: 'settings.privacyPolicy' })
const sections = ['intro', 'dataUse', 'permissions', 'transparency']
return (
+10 -37
View File
@@ -8,16 +8,11 @@ import TemperatureReminder from './temperature-reminder'
import {
periodReminderObservable,
savePeriodReminder,
periodPredictionObservable,
temperatureTrackingCategoryObservable,
} from '../../../local-storage'
import labels from '../../../i18n/en/settings'
import { Alert, Pressable } from 'react-native'
const Reminders = () => {
const isPeriodPredictionDisabled = !periodPredictionObservable.value
const [isPeriodReminderEnabled, setIsPeriodReminderEnabled] = useState(
periodReminderObservable.value.enabled
)
@@ -26,40 +21,18 @@ const Reminders = () => {
savePeriodReminder({ enabled: isEnabled })
}
const reminderDisabledPrompt = () => {
if (!periodPredictionObservable.value) {
Alert.alert(
labels.periodReminder.alertNoPeriodReminder.title,
labels.periodReminder.alertNoPeriodReminder.message
)
}
}
const tempReminderDisabledPrompt = () => {
if (!temperatureTrackingCategoryObservable.value) {
Alert.alert(
labels.tempReminder.alertNoTempReminder.title,
labels.tempReminder.alertNoTempReminder.message
)
}
}
return (
<AppPage>
<Pressable onPress={reminderDisabledPrompt}>
<Segment title={labels.periodReminder.title}>
<AppSwitch
onToggle={periodReminderToggle}
text={labels.periodReminder.reminderText}
value={isPeriodReminderEnabled}
disabled={isPeriodPredictionDisabled}
/>
</Segment>
</Pressable>
<Pressable onPress={tempReminderDisabledPrompt}>
<Segment title={labels.tempReminder.title} last>
<TemperatureReminder />
</Segment>
</Pressable>
<Segment title={labels.tempReminder.title}>
<TemperatureReminder />
</Segment>
<Segment title={labels.periodReminder.title} last>
<AppSwitch
onToggle={periodReminderToggle}
text={labels.periodReminder.reminderText}
value={isPeriodReminderEnabled}
/>
</Segment>
</AppPage>
)
}
@@ -7,7 +7,6 @@ import AppSwitch from '../../common/app-switch'
import {
saveTempReminder,
tempReminderObservable,
temperatureTrackingCategoryObservable,
} from '../../../local-storage'
import padWithZeros from '../../helpers/pad-time-with-zeros'
@@ -52,7 +51,6 @@ const TemperatureReminder = () => {
onToggle={temperatureReminderToggle}
text={tempReminderText}
value={isEnabled}
disabled={!temperatureTrackingCategoryObservable.value}
/>
<DateTimePicker
isVisible={isTimePickerVisible}
+10 -10
View File
@@ -4,21 +4,21 @@ import PropTypes from 'prop-types'
import AppPage from '../common/app-page'
import MenuItem from './menu-item'
import { useTranslation } from 'react-i18next'
import settingsLabels from '../../i18n/en/settings'
const menuItems = [
{ label: 'customization', componentName: 'Customization' },
{ label: 'reminders', componentName: 'Reminders' },
{ label: 'dataManagement', componentName: 'DataManagement' },
{ label: 'password', componentName: 'Password' },
const { menuItems } = settingsLabels
const menu = [
{ ...menuItems.reminders, component: 'Reminders' },
{ ...menuItems.nfpSettings, component: 'NfpSettings' },
{ ...menuItems.dataManagement, component: 'DataManagement' },
{ ...menuItems.password, component: 'Password' },
]
const SettingsMenu = ({ navigate }) => {
const { t } = useTranslation()
return (
<AppPage title={t('hamburgerMenu.settings.title')}>
{menuItems.map((menuItem, i) => {
const last = menuItems.length === i + 1
<AppPage title={settingsLabels.title}>
{menu.map((menuItem, i) => {
const last = menu.length === i + 1
return (
<MenuItem item={menuItem} key={i} last={last} navigate={navigate} />
+3 -9
View File
@@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import AppText from '../common/app-text'
import { formatDecimal } from '../helpers/cycle-day'
import { Sizes, Spacing, Typography } from '../../styles'
const StatsOverview = ({ data }) => {
@@ -15,14 +15,10 @@ StatsOverview.propTypes = {
}
const Row = ({ rowContent }) => {
const isStandardDeviation = rowContent[1].includes('deviation')
if (isStandardDeviation && rowContent[0] !== '—') {
rowContent[0] = formatDecimal(rowContent[0], 1)
}
return (
<View style={styles.row}>
<Cell content={rowContent[0]} isLeft />
<Cell content={rowContent[1]} hasAsterisk={isStandardDeviation} />
<Cell content={rowContent[1]} />
</View>
)
}
@@ -31,7 +27,7 @@ Row.propTypes = {
rowContent: PropTypes.array.isRequired,
}
const Cell = ({ content, isLeft, hasAsterisk }) => {
const Cell = ({ content, isLeft }) => {
const styleContainer = isLeft ? styles.cellLeft : styles.cellRight
const styleText = isLeft ? styles.accentPurpleBig : styles.accentOrange
const numberOfLines = isLeft ? 1 : 2
@@ -45,7 +41,6 @@ const Cell = ({ content, isLeft, hasAsterisk }) => {
style={styleText}
>
{content}
{hasAsterisk && <AppText style={styles.accentOrange}>*</AppText>}
</AppText>
</View>
)
@@ -54,7 +49,6 @@ const Cell = ({ content, isLeft, hasAsterisk }) => {
Cell.propTypes = {
content: PropTypes.node.isRequired,
isLeft: PropTypes.bool,
hasAsterisk: PropTypes.bool,
}
const styles = StyleSheet.create({
@@ -3,7 +3,6 @@ import { FlatList, StyleSheet, View } from 'react-native'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import AppModal from '../common/app-modal'
import AppText from '../common/app-text'
import cycleModule from '../../lib/cycle'
@@ -36,34 +35,26 @@ Item.propTypes = {
data: PropTypes.object.isRequired,
}
const PeriodDetailsModal = ({ onClose }) => {
const StatsTable = () => {
const renderItem = ({ item }) => <Item data={item} />
const data = cycleModule().getStats()
if (!data || data.length === 0) return false
return (
<AppModal onClose={onClose}>
<View>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.date}
ItemSeparatorComponent={ItemDivider}
ListHeaderComponent={FlatListHeader}
ListHeaderComponentStyle={styles.headerDivider}
stickyHeaderIndices={[0]}
contentContainerStyle={styles.container}
/>
</View>
</AppModal>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.date}
ItemSeparatorComponent={ItemDivider}
ListHeaderComponent={FlatListHeader}
ListHeaderComponentStyle={styles.headerDivider}
stickyHeaderIndices={[0]}
contentContainerStyle={styles.container}
/>
)
}
PeriodDetailsModal.propTypes = {
onClose: PropTypes.func,
}
const ItemDivider = () => <View style={styles.divider} />
const FlatListHeader = () => (
@@ -98,7 +89,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: Spacing.tiny,
backgroundColor: 'white',
backgroundColor: Colors.turquoiseLight,
},
cell: {
flex: 2,
@@ -109,10 +100,8 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
container: {
minHeight: '40%',
minWidth: '95%',
paddingHorizontal: Spacing.base,
},
})
export default PeriodDetailsModal
export default StatsTable
+31 -49
View File
@@ -1,25 +1,20 @@
import React, { useState } from 'react'
import { ImageBackground, SafeAreaView, ScrollView, View } from 'react-native'
import React from 'react'
import { ImageBackground, View } from 'react-native'
import { ScaledSheet } from 'react-native-size-matters'
import { useTranslation } from 'react-i18next'
import AppText from '../common/app-text'
import Button from '../common/button'
import Footnote from '../common/Footnote'
import StatsOverview from './StatsOverview'
import PeriodDetailsModal from './PeriodDetailsModal'
import StatsTable from './StatsTable'
import cycleModule from '../../lib/cycle'
import { getCycleLengthStats as getCycleInfo } from '../../lib/cycle-length'
import { formatDecimal } from '../helpers/cycle-day'
import { Containers, Sizes, Spacing, Typography } from '../../styles'
const image = require('../../assets/cycle-icon.png')
const Stats = () => {
const [isStatsVisible, setIsStatsVisible] = useState(false)
const { t } = useTranslation(null, { keyPrefix: 'stats' })
const cycleLengths = cycleModule().getAllCycleLengths()
@@ -28,7 +23,6 @@ const Stats = () => {
numberOfCycles > 0
? getCycleInfo(cycleLengths)
: { minimum: '—', maximum: '—', stdDeviation: '—' }
const standardDeviation = cycleData.stdDeviation
? cycleData.stdDeviation
: '—'
@@ -39,55 +33,43 @@ const Stats = () => {
[numberOfCycles, t('overview.completedCycles')],
]
if (cycleData.mean) {
cycleData.mean = formatDecimal(cycleData.mean, 1)
}
return (
<SafeAreaView style={styles.pageContainer}>
<ScrollView contentContainerStyle={styles.overviewContainer}>
<View style={styles.pageContainer}>
<View style={styles.overviewContainer}>
<AppText>{t('intro')}</AppText>
{numberOfCycles === 0 ? (
<AppText>{t('noData')}</AppText>
) : (
<>
<View style={styles.container}>
<View style={styles.columnLeft}>
<ImageBackground
source={image}
imageStyle={styles.image}
style={styles.imageContainter}
<View style={styles.container}>
<View style={styles.columnLeft}>
<ImageBackground
source={image}
imageStyle={styles.image}
style={styles.imageContainter}
>
<AppText
numberOfLines={1}
ellipsizeMode="clip"
style={styles.accentPurpleGiant}
>
<AppText
numberOfLines={1}
ellipsizeMode="clip"
style={styles.accentPurpleGiant}
>
{cycleData.mean}
</AppText>
<AppText style={styles.accentPurpleHuge}>
{t('overview.days')}
</AppText>
</ImageBackground>
<AppText style={styles.accentOrange}>
{t('overview.average')}
{cycleData.mean}
</AppText>
</View>
<View style={styles.columnRight}>
<StatsOverview data={statsData} />
</View>
<AppText style={styles.accentPurpleHuge}>
{t('overview.days')}
</AppText>
</ImageBackground>
<AppText style={styles.accentOrange}>
{t('overview.average')}
</AppText>
</View>
<Button isCTA onPress={() => setIsStatsVisible(true)}>
{t('showStats')}
</Button>
{isStatsVisible && (
<PeriodDetailsModal onClose={() => setIsStatsVisible(false)} />
)}
<Footnote>{t('footnote')}</Footnote>
</>
<View style={styles.columnRight}>
<StatsOverview data={statsData} />
</View>
</View>
)}
</ScrollView>
</SafeAreaView>
</View>
<StatsTable />
</View>
)
}
+8 -5
View File
@@ -1,6 +1,10 @@
import { PixelRatio, StatusBar } from 'react-native'
import { scale, verticalScale } from 'react-native-size-matters'
export const ACTION_DELETE = 'delete'
export const ACTION_EXPORT = 'export'
export const ACTION_IMPORT = 'import'
export const SYMPTOMS = [
'bleeding',
'temperature',
@@ -15,13 +19,12 @@ export const SYMPTOMS = [
export const CHART_COLUMN_WIDTH = 32
export const CHART_COLUMN_MIDDLE = CHART_COLUMN_WIDTH / 2
export const CHART_DOT_RADIUS_SYMPTOM = scale(6)
export const CHART_DOT_RADIUS_TEMPERATURE = scale(4)
export const CHART_DOT_RADIUS = scale(6)
export const CHART_GRID_LINE_HORIZONTAL_WIDTH =
PixelRatio.roundToNearestPixel(0.3)
export const CHART_ICON_SIZE = scale(20)
export const CHART_STROKE_WIDTH = scale(1.5)
export const CHART_SYMPTOM_HEIGHT_RATIO = scale(0.06)
export const CHART_STROKE_WIDTH = scale(3)
export const CHART_SYMPTOM_HEIGHT_RATIO = scale(0.08)
export const CHART_XAXIS_HEIGHT_RATIO = scale(0.1)
export const CHART_YAXIS_WIDTH = scale(32)
export const CHART_TICK_WIDTH = scale(44)
@@ -37,7 +40,7 @@ export const HIT_SLOP = {
top: verticalScale(20),
bottom: verticalScale(20),
left: scale(20),
right: scale(20),
right: scale(20)
}
export const STATUSBAR_HEIGHT = StatusBar.currentHeight
+11
View File
@@ -1,5 +1,6 @@
import Realm from 'realm'
import { LocalDate, ChronoUnit } from '@js-joda/core'
import nodejs from 'nodejs-mobile-react-native'
import fs from 'react-native-fs'
import schemas from './schemas'
@@ -183,6 +184,16 @@ export function tryToImportWithoutDelete(cycleDays) {
})
}
export function requestHash(type, pw) {
nodejs.channel.post(
'request-SHA512',
JSON.stringify({
type: type,
message: pw,
})
)
}
export async function changeDbEncryption(hash) {
let key
if (hash) key = hashToInt8Array(hash)
+24
View File
@@ -0,0 +1,24 @@
import React, { createContext, useContext, useState } from 'react'
import PropTypes from 'prop-types'
import { LocalDate } from '@js-joda/core'
const DateContext = createContext()
export const DateProvider = ({ children }) => {
const [date, setDate] = useState(LocalDate.now().toString())
return (
<DateContext.Provider value={{ date, setDate }}>
{children}
</DateContext.Provider>
)
}
DateProvider.propTypes = {
children: PropTypes.node,
}
export const useDate = () => {
const { date, setDate } = useContext(DateContext)
return { date, setDate }
}
+2 -78
View File
@@ -1,23 +1,4 @@
{
"bottomMenu": {
"calendar": "Calendar",
"chart": "Chart",
"stats": "Stats"
},
"chart": {
"tutorial": "You can swipe the chart to view more dates."
},
"symptoms": {
"bleeding": "bleeding",
"temperature": "temperature",
"mucus": "cervical mucus",
"cervix": "cervix",
"note": "note",
"desire": "desire",
"sex": "sex",
"pain": "pain",
"mood": "mood"
},
"labels": {
"bleedingPrediction": {
"noPrediction": "As soon as you have tracked 3 menstrual cycles, drip. will make predictions for the next ones."
@@ -32,7 +13,7 @@
"ok": "OK"
}
},
"hamburgerMenu": {
"settings": {
"about": {
"credits": {
"title": "Credits",
@@ -49,7 +30,7 @@
},
"philosophy": {
"title": "Remember to think for yourself",
"text": "drip. makes period predictions for you and helps you apply the sympto-thermal method for fertility awareness. 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."
},
"title": "About",
"version": {
@@ -60,12 +41,6 @@
"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:"
},
"menuMain": {
"about": "About",
"license": "License",
"settings": "Settings",
"privacyPolicy": "Privacy Policy"
},
"privacyPolicy": {
"title": "Privacy Policy",
"intro": {
@@ -84,57 +59,6 @@
"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!"
}
},
"settings": {
"data": {
"import": {
"button": "Import data",
"dialog": {
"cancel": "Cancel",
"delete": "Import and delete existing",
"message": "There are two options for the import:\n\n1. Keep existing cycle days and replace only the ones in the import file.\n\n2. Delete all existing cycle days and import cycle days from file",
"replace": "Import and replace",
"title": "Keep existing data?"
},
"error": {
"couldNotOpenFile": "Could not open file",
"futureEdit": "Future dates may only contain a note, no other symptoms",
"incorrectColumns": "Expected CSV column titles to be {{incorrectColumns}}",
"noDataImported": "{{message}}\n\nNo data was imported or changed"
},
"segmentExplainer": "Import data in CSV format",
"success": {
"message": "Data successfully imported",
"title": "Success"
}
}
},
"menuItem": {
"dataManagement": {
"name": "Data",
"text": "Import, export or delete your data"
},
"customization": {
"name": "Customization",
"text": "Define how you want to use drip"
},
"password": {
"name": "Password",
"text": "Set, edit or delete your password"
},
"reminders": {
"name": "Reminders",
"text": "Turn on/off reminders"
},
"info": {
"name": "Info",
"text": "Learn more about how drip works"
}
},
"title": "Settings"
},
"info": {
"title": "info"
}
},
"stats": {
+43 -46
View File
@@ -4,71 +4,70 @@ export const intensity = ['low', 'medium', 'high']
export const bleeding = {
labels: ['spotting', 'light', 'medium', 'heavy'],
heaviness: {
header: 'Heaviness',
explainer: 'How heavy is the bleeding?',
header: "Heaviness",
explainer: "How heavy is the bleeding?",
},
exclude: {
header: 'Exclude',
explainer: "You can exclude this value if it's not menstrual bleeding",
},
header: "Exclude",
explainer: "You can exclude this value if it's not menstrual bleeding"
}
}
export const cervix = {
subcategories: {
opening: 'opening',
firmness: 'firmness',
position: 'position',
position: 'position'
},
opening: {
categories: ['closed', 'medium', 'open'],
explainer: 'Is your cervix open or closed?',
explainer: 'Is your cervix open or closed?'
},
firmness: {
categories: ['hard', 'soft'],
explainer: "When it's hard, it might feel like the tip of your nose",
explainer: "When it's hard, it might feel like the tip of your nose"
},
position: {
categories: ['low', 'medium', 'high'],
explainer: 'How high up in the vagina is the cervix?',
explainer: 'How high up in the vagina is the cervix?'
},
excludeExplainer:
"You can exclude this value if you don't want to use it for fertility detection.",
excludeExplainer: "You can exclude this value if you don't want to use it for fertility detection.",
actionHint: 'Choose values for at least "Opening" and "Firmness" to save.'
}
export const mucus = {
subcategories: {
feeling: 'feeling',
texture: 'texture',
texture: 'texture'
},
feeling: {
categories: ['dry', 'nothing', 'wet', 'slippery'],
explainer: 'What does your vaginal entrance feel like?',
explainer: 'What does your vaginal entrance feel like?'
},
texture: {
categories: ['nothing', 'creamy', 'egg white'],
explainer:
'Looking at and touching your cervical mucus, which describes it best?',
explainer: "Looking at and touching your cervical mucus, which describes it best?"
},
excludeExplainer:
"You can exclude this value if you don't want to use it for fertility detection",
excludeExplainer: "You can exclude this value if you don't want to use it for fertility detection",
actionHint: 'Choose values for both "Feeling" and "Texture" to save.'
}
export const desire = {
header: 'Intensity',
explainer: 'How would you rate your sexual desire?',
explainer: 'How would you rate your sexual desire?'
}
export const sex = {
categories: {
categories:{
solo: 'solo',
partner: 'partner',
},
header: 'Activity',
header: "Activity",
explainer: 'Were you sexually active today?',
}
export const contraceptives = {
categories: {
categories:{
condom: 'condom',
pill: 'pill',
iud: 'iud',
@@ -79,8 +78,8 @@ export const contraceptives = {
none: 'none',
other: 'other',
},
header: 'Contraceptives',
explainer: 'Did you use contraceptives?',
header: "Contraceptives",
explainer: 'Did you use contraceptives?'
}
export const pain = {
@@ -92,9 +91,9 @@ export const pain = {
nausea: 'nausea',
tenderBreasts: 'tender breasts',
migraine: 'migraine',
other: 'other',
other: 'other'
},
explainer: 'How did your body feel today?',
explainer: 'How did your body feel today?'
}
export const mood = {
@@ -108,39 +107,37 @@ export const mood = {
energetic: 'energetic',
fatigue: 'fatigue',
angry: 'angry',
other: 'other',
other: 'other'
},
explainer: 'How did you feel today?',
explainer: 'How did you feel today?'
}
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.',
outOfAbsoluteRangeWarning:
'This temperature value is too high or low to be shown on the temperature chart.',
// 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.',
saveAnyway: 'Save anyway',
temperature: {
header: 'Temperature',
explainer:
'Take your temperature right after waking up, before getting out of bed',
header: "Temperature",
explainer: 'Take your temperature right after waking up, before getting out of bed'
},
time: 'Time',
time: "Time",
note: {
header: 'Note',
explainer:
'Is there anything that could have influenced this value, such as bad sleep or alcohol consumption?',
header: "Note",
explainer: 'Is there anything that could have influenced this value, such as bad sleep or alcohol consumption?'
},
exclude: {
header: 'Exclude',
explainer:
"You can exclude this value if you don't want to use it for fertility detection",
},
header: "Exclude",
explainer: "You can exclude this value if you don't want to use it for fertility detection"
}
}
export const noteExplainer = 'Anything you want to add for the day?'
export const noteExplainer = "Anything you want to add for the day?"
export const general = {
cycleDayNumber: 'Cycle day ',
today: 'Today',
cycleDayNumber: "Cycle day ",
today: "Today"
}
export const sharedDialogs = {
@@ -150,5 +147,5 @@ export const sharedDialogs = {
reallyDeleteData: 'Yes, I am sure',
save: 'Save',
delete: 'Delete',
disabledInfo: 'There is some data missing',
disabledInfo: 'There is some data missing'
}
+34 -3
View File
@@ -1,8 +1,15 @@
import labels from './settings'
const settingsTitles = labels.menuItems
export const home = {
unknown: '?',
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
}
export const chart = {
tutorial: 'You can swipe the chart to view more dates.',
}
export const shared = {
cancel: 'Cancel',
save: 'Save',
@@ -26,6 +33,30 @@ export const shared = {
learnMore: 'Learn more',
}
export const headerTitles = {
Home: 'Home',
Calendar: 'Calendar',
Chart: 'Chart',
Stats: 'Statistics',
SettingsMenu: 'Settings',
Reminders: settingsTitles.reminders.name,
NfpSettings: settingsTitles.nfpSettings.name,
DataManagement: settingsTitles.dataManagement.name,
Password: settingsTitles.password.name,
About: 'About',
License: 'License',
PrivacyPolicy: 'Privacy Policy',
bleeding: 'Bleeding',
temperature: 'Temperature',
mucus: 'Cervical Mucus',
cervix: 'Cervix',
note: 'Note',
desire: 'Desire',
sex: 'Sex',
pain: 'Pain',
mood: 'Mood',
}
export const stats = {
cycleLengthExplainer: 'Basic statistics about the length of your cycles.',
emptyStats: 'At least one completed cycle is needed to display stats.',
@@ -70,14 +101,14 @@ export const fertilityStatus = {
unknown:
'We cannot show any cycle information because no period data has been added.',
preOvuText:
"According to the sympto-thermal method, 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.",
"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 (
'We detected a temperature shift (' +
['regular', '1st exception', '2nd exception'][tempRule] +
' temperature rule), as well as a cervical mucus/cervix shift according to the sympto-thermal method. 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.'
)
},
@@ -85,7 +116,7 @@ export const fertilityStatus = {
return (
'We detected a temperature shift (' +
['regular', '1st exception', '2nd exception'][tempRule] +
' temperature rule), as well as a cervical mucus/cervix shift according to the sympto-thermal method. 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.'
)
},
+45 -47
View File
@@ -1,10 +1,28 @@
import links from './links'
export default {
customization: {
title: 'Customization',
trackingCategories: 'Tracking categories',
subheaderSymptoThermalMethod: 'Sympto-thermal method settings',
title: 'Settings',
menuItems: {
reminders: {
name: 'Reminders',
text: 'turn on/off reminders',
},
nfpSettings: {
name: 'NFP settings',
text: 'define how you want to use NFP',
},
dataManagement: {
name: 'Data',
text: 'import, export or delete your data',
},
password: {
name: 'Password',
text: '',
},
about: 'About',
license: 'License',
settings: 'Settings',
privacyPolicy: 'Privacy Policy',
},
export: {
errors: {
@@ -18,6 +36,24 @@ export default {
segmentExplainer:
'Export data in CSV format for backup or so you can use it elsewhere',
},
import: {
button: 'Import data',
title: 'Keep existing data?',
message: `There are two options for the import:
1. Keep existing cycle days and replace only the ones in the import file.
2. Delete all existing cycle days and import cycle days from file.`,
replaceOption: 'Import and replace',
deleteOption: 'Import and delete existing',
errors: {
couldNotOpenFile: 'Could not open file',
postFix: 'No data was imported or changed',
futureEdit: 'Future dates may only contain a note, no other symptoms',
},
success: {
message: 'Data successfully imported',
},
segmentExplainer: 'Import data in CSV format',
},
deleteSegment: {
title: 'Delete app data',
explainer: 'Delete app data from this phone',
@@ -37,26 +73,17 @@ export default {
tempScale: {
segmentTitle: 'Temperature scale',
segmentExplainer:
'Change the minimum and maximum value for the temperature chart.',
'Change the minimum and maximum value for the temperature chart',
min: 'Min',
max: 'Max',
loadError: 'Could not load saved temperature scale settings',
saveError: 'Could not save temperature scale settings',
disabled: 'Disabled',
disabledMessage:
'To use the temperature scale please first enable temperature tracking above.',
},
tempReminder: {
title: 'Temperature reminder',
noTimeSet: 'Set a time for a daily reminder to take your temperature',
timeSet: (time) => `Daily reminder set for ${time}`,
notification: 'Record your morning temperature',
alertNoTempReminder: {
title: 'Temperature turned off',
message:
'To use the temperature reminder please first enable the temperature tracking category in the customization settings.',
},
},
periodReminder: {
title: 'Next period reminder',
@@ -64,42 +91,13 @@ export default {
'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.`,
alertNoPeriodReminder: {
title: 'Period predictions turned off',
message:
'To use the period reminder please first enable period predictions in the customization settings.',
},
},
fertilityTracking: {
title: 'Fertility phases calculation',
disabledTitle: 'Disabled',
disabled:
'To use fertility phases calculation please enable both temperature tracking and either cervical mucus or cervix tracking above.',
message:
'If you enter menstrual bleeding, temperature and cervical mucus or cervix data according to the sympto-thermal method, drip will calculate cycle phases with the provided data.',
on: 'If you switch this off, drip will not show fertility related information.',
off: 'If you switch this on, drip will show fertility related information.',
},
secondarySymptom: {
useCervix: {
title: 'Secondary symptom',
cervixModeOn:
'Cervix values are being used for fertility detection according to the sympto-thermal method.',
'Cervix values are being used for symptothermal fertility detection. You can switch here to use cervical mucus values for symptothermal fertility detection',
cervixModeOff:
'Cervical mucus values are being used for fertility detection according to the sympto-thermal method.',
disabled: {
title: 'Disabled',
message:
'To set a secondary symptom please first enable the cervical mucus or cervix tracking category as well as temperature and fertility phases calculation above.',
noSecondaryEnabled:
'To switch the secondary symptom both cervical mucus and cervix need to be enabled above.',
},
mucus: 'cervical mucus',
cervix: 'cervix',
},
periodPrediction: {
title: 'Period predictions',
on: 'drip predicts your 3 next menstrual bleedings based on statistics if you previously tracked at least 3 complete cycles.',
off: 'There are no predictions for menstrual cycles displayed. If turned on, the calendar and the home screen will display period predictions.',
'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: {
title: 'App password',
@@ -142,6 +140,6 @@ Making any changes to your password setting will keep your data as it was before
},
preOvu: {
title: 'Infertile days at cycle start',
note: `drip. applies the sympto-thermal method 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 sympto-thermal 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.`,
},
}
+8 -8
View File
@@ -11,7 +11,7 @@ export const generalInfo = {
3. and menstrual bleeding
the app helps you identify in which phase of the menstrual cycle you are.
drip. makes period predictions for you and helps you apply the sympto-thermal method for fertility awareness. 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}.`,
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.`,
@@ -25,9 +25,9 @@ export default {
After tracking at least 3 menstrual cycles, drip. will give you an overview of
· how long your cycles last on average (in "stats"),
· 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") if this functionality is enabled in the customization settings.
· and predict your next 3 cycles with a range of 3 or 5 days (on home screen and "calendar").
The app allows you to track different intensities of bleeding. On the chart and on the calendar, bleeding values are colored in different shades of red. The darker, the more intense your bleeding. Every bleeding value that is not excluded is taken into account for fertility calculation and period predictions.
The app allows you to track different intensities of bleeding. On the chart and on the calendar, bleeding values are colored in different shades of red. The darker, the more intense your bleeding. Every bleeding value that is not excluded is taken into account for fertility calculation and prediction for the start of next cycles.
Excluding bleeding values is for tracking bleeding when it's not marking the start of a new cycle or the continuation of menstrual bleeding the day(s) before, e.g. bleeding caused by ovulation or a miscarriage.
@@ -39,7 +39,7 @@ ${generalInfo.nfpTfyReminder}`,
Tracking how open or closed and how firm or soft the cervix feels can help determine in which phase of the menstrual cycle you are.
By default, the secondary symptom the app uses for the sympto-thermal method is cervical mucus, but you can change it to cervix in "Settings" -> "Customization".
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?
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.
@@ -74,10 +74,10 @@ ${generalInfo.curiousNfp}`,
title: 'Tracking cervical mucus',
text: `Cervical mucus can help determine in which phase of the menstrual cycle you are.
By default the secondary symptom the app uses for the sympto-thermal method is cervical mucus. You can change this in the customization settings.
By default the secondary symptom the app uses for NFP evaluation is 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 values following the sympto-thermal method.
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:
· t = (dry feeling + no texture),
· = (no feeling + no texture),
@@ -87,7 +87,7 @@ 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.
Please note that drip. does not yet support "parenthesis values": According to the sympto-thermal method, 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}
@@ -125,7 +125,7 @@ ${generalInfo.curiousNfp}`,
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.
By default the secondary symptom is cervical mucus, but you can change it to cervix in "Settings" -> "Customization".
By default the secondary symptom is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings".
· What is body basal temperature?
It's your temperature after lying still for at least 6 hours. For many, this is when they are waking up in the morning after sleeping at least 6 hours and before getting up.
+1
View File
@@ -17,6 +17,7 @@ i18n
compatibilityJSON: 'v3', // TODO: migrate json to v4 and afterwards remove it
resources,
fallbackLng: 'en',
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
+3 -15
View File
@@ -2,35 +2,23 @@ require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, '11.0'
install! 'cocoapods', :deterministic_uuids => false
target 'drip' do
config = use_native_modules!
# Flags change depending on the env values.
flags = get_default_flags()
use_react_native!(
:path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => flags[:hermes_enabled],
:fabric_enabled => flags[:fabric_enabled],
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
:hermes_enabled => false
)
target 'dripTests' do
inherit! :complete
# Pods for testing
end
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
use_flipper!()
post_install do |installer|
react_native_post_install(installer)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
end
end
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1210"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+1 -2
View File
@@ -1,8 +1,7 @@
#import <React/RCTBridgeDelegate.h>
#import <UIKit/UIKit.h>
#import <UserNotifications/UNUserNotificationCenter.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
@interface AppDelegate : UIResponder <UIApplicationDelegate, UNUserNotificationCenterDelegate>
@property (nonatomic, strong) UIWindow *window;
+100
View File
@@ -0,0 +1,100 @@
#import <AppDelegate.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
// #ifdef FB_SONARKIT_ENABLED
// #import <FlipperKit/FlipperClient.h>
// #import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
// #import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
// #import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
// #import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
// #import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
// static void InitializeFlipper(UIApplication *application) {
// FlipperClient *client = [FlipperClient sharedClient];
// SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
// [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
// [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
// [client addPlugin:[FlipperKitReactPlugin new]];
// [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
// [client start];
// }
// #endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// #ifdef FB_SONARKIT_ENABLED
// InitializeFlipper(application);
// #endif
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"drip"
initialProperties:nil];
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
// Define UNUserNotificationCenter
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
return YES;
}
//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}
// Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
[RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Required for the notification event. You must call the completion handler after handling the remote notification.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
[RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
// Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
[RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error];
}
// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
[RNCPushNotificationIOS didReceiveNotificationResponse:response];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
@end
-108
View File
@@ -1,108 +0,0 @@
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTAppSetupUtils.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/CoreModulesPlugins.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <react/config/ReactNativeConfig.h>
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
RCTTurboModuleManager *_turboModuleManager;
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
facebook::react::ContextContainer::Shared _contextContainer;
}
@end
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTAppSetupPrepareApp(application);
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
#if RCT_NEW_ARCH_ENABLED
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
#endif
UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"drip", nil);
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTCxxBridgeDelegate
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:self
jsInvoker:bridge.jsCallInvoker];
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
}
#pragma mark RCTTurboModuleManagerDelegate
- (Class)getModuleClassFromName:(const char *)name
{
return RCTCoreModulesClassProvider(name);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return nullptr;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
initParams:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
return RCTAppSetupDefaultModuleFromClass(moduleClass);
}
#endif
@end
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

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