Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd96baec22 | |||
| 93f412e983 | |||
| a5b058486e | |||
| 95cf6f6001 | |||
| 540d6aee03 | |||
| 0f7e3fe0ad | |||
| c21fd193ce | |||
| d5dc4ada12 | |||
| 01582c4e16 | |||
| e6134b4592 | |||
| 3b34ce7134 | |||
| d86f3ec9eb | |||
| 583c9c032c | |||
| 9fbe99e23b |
+1
-1
@@ -30,7 +30,7 @@ ios/Index/DataStore
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
*.properties
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
|
||||
|
||||
@@ -34,31 +34,13 @@ or clone it with HTTPS
|
||||
|
||||
git clone https://gitlab.com/bloodyhealth/drip.git
|
||||
|
||||
### 2. Node version
|
||||
### 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:
|
||||
|
||||
nvm install v14.19.3
|
||||
|
||||
#### On Apple Silicon M1
|
||||
|
||||
NodeJS 14 does not compile on the M1 architecture, so it has to be installed through Rosetta: https://devzilla.io/using-nodejs-14-with-mac-silicon-m1 .
|
||||
To activate Rosetta and switch to intel emulation run:
|
||||
|
||||
arch -x86_64 zsh
|
||||
|
||||
Run
|
||||
|
||||
arch
|
||||
|
||||
again to verify that it returns "i386".
|
||||
Now install node 14:
|
||||
|
||||
nvm install v14.19.3
|
||||
|
||||
### 3. Yarn version
|
||||
|
||||
use npm to install yarn:
|
||||
|
||||
npm install --global yarn
|
||||
@@ -131,36 +113,20 @@ Minimum system requirements to run iOS app are as follows:
|
||||
- MacOS 10.15.7 for Mac users
|
||||
- Xcode 13 (command line tools only might be enough)
|
||||
|
||||
i. Install yarn dependencies
|
||||
|
||||
yarn install ..
|
||||
|
||||
ii. Install XCode dependencies by running the following command from the root project directory:
|
||||
i. Install XCode dependencies by running the following command from the root project directory:
|
||||
|
||||
cd ios && pod install && cd ..
|
||||
|
||||
iii. To run app either open drip workspace ('drip.xcworkspace' file) with XCode and run "Build" or run the following command:
|
||||
ii. To run app either open drip workspace ('drip.xcworkspace' file) with XCode and run "Build" or run the following command:
|
||||
|
||||
yarn ios
|
||||
|
||||
iiii. If you are building the app with XCode make sure you are running this as well:
|
||||
iii. If you are building the app with XCode make sure you are running this as well:
|
||||
|
||||
yarn start
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### [MacOS M1] Flipper problems
|
||||
|
||||
If a bug in the currently used Flipper version prevents building the project, comment out the respective line in the podfile, like so:
|
||||
|
||||
#use_flipper!()
|
||||
|
||||
Run
|
||||
|
||||
pod install
|
||||
|
||||
from the ios directory again to reload the dependencies.
|
||||
|
||||
#### [MacOS] Java problems
|
||||
|
||||
Make sure that you have Java 1.8 by running `java -version`.
|
||||
|
||||
+36
-61
@@ -7,38 +7,27 @@ drip is developed in React Native for iOS and Android and is released on 4 diffe
|
||||
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 the past this has never been the case. The release v1.2403.19 is the first to be up to date on all 4 platforms!
|
||||
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
|
||||
### Release steps
|
||||
|
||||
### 1. [Code](#code)
|
||||
- 1.1 [Version updating](#version-updating)
|
||||
- 1.2 [Android builds](#android-builds)
|
||||
- 1.3 [iOS builds](#ios-builds)
|
||||
- 1.4 [User testing](#user-testing)
|
||||
- 1.5 [Release tag](#release-tag)
|
||||
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)
|
||||
|
||||
### 2. [Documentation](#documentation)
|
||||
- 2.1 [Changelog](#changelog)
|
||||
- 2.2 [Release notes](#release-notes)
|
||||
- 2.3 [Releases on Gitlab](#releases-on-gitlab)
|
||||
- 2.4 [Phone screenshots](#phone-screenshots)
|
||||
|
||||
### 3. [Publishing](#publishing)
|
||||
- 3.1 [Google Play Console](#google-play-console)
|
||||
- 3.2 [Apple App Store Connect](#apple-app-store-connect)
|
||||
- 3.3 [F-droid](#f-droid)
|
||||
- 3.4 [drip website](#drip-website)
|
||||
- 3.5 [Communication](#communication)
|
||||
- 3.6 [Self care](#self-care)
|
||||
|
||||
## Code
|
||||
|
||||
### Version updating
|
||||
## 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.
|
||||
|
||||
@@ -123,50 +112,42 @@ Once the archiving process has completed you can chose to do the following:
|
||||
- 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
|
||||
## 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).
|
||||
|
||||
### 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".
|
||||
|
||||
Any tag starting with "Release" or "Android" will be checked by https://gitlab.com/fdroidci
|
||||
|
||||
## Documentation
|
||||
|
||||
### Changelog
|
||||
## 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
|
||||
**v1.yymm.d**
|
||||
|
||||
### Release notes
|
||||
**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.
|
||||
|
||||
### Releases on Gitlab
|
||||
## Release tag
|
||||
|
||||
Under [Releases](https://gitlab.com/bloodyhealth/drip/-/releases) we keep track of all drip releases.
|
||||
[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
|
||||
## 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).
|
||||
|
||||
@@ -183,17 +164,11 @@ You can decide if you want the new app version to get released for testing (inte
|
||||
|
||||
Upload a new version and submit it for review, before it can be published.
|
||||
|
||||
### F-droid
|
||||
|
||||
This account runs automated checks for drip looking at new `Release` or `Android` [tags](https://gitlab.com/bloodyhealth/drip/-/tags/) and updates the app's metadata yaml file in Fdroid without further ado.
|
||||
|
||||
However this is not the full story. Please have a look at previous commits to see what necessary changes got pushed, [see here](https://gitlab.com/fdroid/fdroiddata/-/commits/master/metadata/com.drip.yml).
|
||||
|
||||
### 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
|
||||
## Communication
|
||||
|
||||
You probably want to share the app update by posting on one or more of these platforms:
|
||||
|
||||
@@ -203,6 +178,6 @@ You probably want to share the app update by posting on one or more of these pla
|
||||
- [Linkedin](https://www.linkedin.com/company/34899684/)
|
||||
- Different tech, privacy, feminist oriented slacks
|
||||
|
||||
### Self care
|
||||
## Self care
|
||||
|
||||
Congratulations. Take a break, eat some chocolate, go see a live show of your favorite band, masturbate <3!
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
apply plugin: "com.android.application"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
/**
|
||||
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||
@@ -127,16 +125,6 @@ def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
*/
|
||||
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
|
||||
|
||||
// Create a variable called keystorePropertiesFile, and initialize it to your
|
||||
// keystore.properties file, in the rootProject folder.
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
||||
// Initialize a new Properties() object called keystoreProperties.
|
||||
def keystoreProperties = new Properties()
|
||||
|
||||
// Load your keystore.properties file into the keystoreProperties object.
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
@@ -146,8 +134,8 @@ android {
|
||||
applicationId "com.drip"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 38
|
||||
versionName "1.2410.22"
|
||||
versionCode 35
|
||||
versionName "1.2409.26"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
@@ -162,10 +150,6 @@ android {
|
||||
keyPassword 'android'
|
||||
}
|
||||
release {
|
||||
storeFile file('drip-release-key.keystore')
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storePassword keystoreProperties['storePassword']
|
||||
if (project.hasProperty('DRIP_RELEASE_STORE_FILE')) {
|
||||
storeFile file(DRIP_RELEASE_STORE_FILE)
|
||||
storePassword DRIP_RELEASE_STORE_PASSWORD
|
||||
|
||||
@@ -53,7 +53,6 @@ ext {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
soLoaderVersion = "0.10.4+"
|
||||
|
||||
if (System.properties['os.arch'] == "aarch64") {
|
||||
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
||||
|
||||
@@ -23,20 +23,14 @@ const SymptomBox = ({
|
||||
const { t } = useTranslation(null, { keyPrefix: 'symptoms' })
|
||||
const isSymptomEdited = editedSymptom === symptom
|
||||
const isSymptomDisabled = isDateInFuture(date) && symptom !== 'note'
|
||||
const isExcluded = symptomData !== null ? symptomData.exclude : false
|
||||
|
||||
const iconColor = isSymptomDisabled ? Colors.greyLight : Colors.grey
|
||||
const iconName = `drip-icon-${symptom}`
|
||||
const symptomNameStyle = [
|
||||
styles.symptomName,
|
||||
isSymptomDisabled && styles.symptomNameDisabled,
|
||||
isExcluded && styles.symptomNameExcluded,
|
||||
]
|
||||
const textStyle = [
|
||||
styles.text,
|
||||
isSymptomDisabled && styles.textDisabled,
|
||||
isExcluded && styles.textExcluded,
|
||||
]
|
||||
const textStyle = [styles.text, isSymptomDisabled && styles.textDisabled]
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -63,11 +57,11 @@ const SymptomBox = ({
|
||||
/>
|
||||
<View style={styles.textContainer}>
|
||||
<AppText style={symptomNameStyle}>{t(symptom)}</AppText>
|
||||
{symptomDataToDisplay && (
|
||||
{symptomDataToDisplay ? (
|
||||
<AppText style={textStyle} numberOfLines={4}>
|
||||
{symptomDataToDisplay}
|
||||
</AppText>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
@@ -121,9 +115,6 @@ const styles = StyleSheet.create({
|
||||
textDisabled: {
|
||||
color: Colors.greyLight,
|
||||
},
|
||||
textExcluded: {
|
||||
color: Colors.grey,
|
||||
},
|
||||
})
|
||||
|
||||
export default SymptomBox
|
||||
|
||||
@@ -12,7 +12,7 @@ import SelectBoxGroup from './select-box-group'
|
||||
import SelectTabGroup from './select-tab-group'
|
||||
import Temperature from './temperature'
|
||||
|
||||
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
|
||||
import { blank, save, shouldShow, symptomPage } from '../helpers/cycle-day'
|
||||
import { showToast } from '../helpers/general'
|
||||
|
||||
import { fertilityTrackingObservable } from '../../local-storage'
|
||||
@@ -21,9 +21,10 @@ import info from '../../i18n/en/symptom-info'
|
||||
import { Colors, Containers, Sizes, Spacing } from '../../styles'
|
||||
|
||||
const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
|
||||
const symptomConfig = symtomPage[symptom]
|
||||
const symptomConfig = symptomPage[symptom]
|
||||
const [data, setData] = useState(symptomData ? symptomData : blank[symptom])
|
||||
const [shouldShowInfo, setShouldShowInfo] = useState(false)
|
||||
const isBleeding = symptom === 'bleeding'
|
||||
const getParsedData = () => JSON.parse(JSON.stringify(data))
|
||||
const onPressLearnMore = () => setShouldShowInfo(!shouldShowInfo)
|
||||
const isFertilityTrackingEnabled = fertilityTrackingObservable.value
|
||||
@@ -117,6 +118,23 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
|
||||
textAlignVertical: 'top',
|
||||
}
|
||||
|
||||
{
|
||||
/* show exclude AppSwitch for bleeding, mucus, cervix, temperature */
|
||||
}
|
||||
{
|
||||
/* but if fertility is off only for bleeding */
|
||||
}
|
||||
const excludeToggle = shouldShow(symptomConfig.excludeText) &&
|
||||
(isBleeding || isFertilityTrackingEnabled) && (
|
||||
<Segment style={styles.segmentBorder}>
|
||||
<AppSwitch
|
||||
onToggle={onExcludeToggle}
|
||||
text={symptomPage[symptom].excludeText}
|
||||
value={data.exclude}
|
||||
/>
|
||||
</Segment>
|
||||
)
|
||||
|
||||
return (
|
||||
<AppModal onClose={onSave}>
|
||||
<ScrollView
|
||||
@@ -130,10 +148,16 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
|
||||
save={(value, field) => onSaveTemperature(value, field)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* There should not be a line between the bleeding tab group and the exclude toggle */}
|
||||
{shouldShow(symptomConfig.selectTabGroups) &&
|
||||
symtomPage[symptom].selectTabGroups.map((group) => {
|
||||
symptomPage[symptom].selectTabGroups.map((group) => {
|
||||
return (
|
||||
<Segment key={group.key} style={styles.segmentBorder}>
|
||||
<Segment
|
||||
key={group.key}
|
||||
style={styles.segmentBorder}
|
||||
last={isBleeding}
|
||||
>
|
||||
<AppText style={styles.title}>{group.title}</AppText>
|
||||
<SelectTabGroup
|
||||
activeButton={data[group.key]}
|
||||
@@ -143,8 +167,12 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
|
||||
</Segment>
|
||||
)
|
||||
})}
|
||||
|
||||
{/*for bleeding, we want to move the "exclude" toggle up between the tab and box groups, all other symptoms should still have it at the bottom*/}
|
||||
{isBleeding && excludeToggle}
|
||||
|
||||
{shouldShow(symptomConfig.selectBoxGroups) &&
|
||||
symtomPage[symptom].selectBoxGroups.map((group) => {
|
||||
symptomPage[symptom].selectBoxGroups.map((group) => {
|
||||
const isOtherSelected =
|
||||
data['other'] !== null &&
|
||||
data['other'] !== false &&
|
||||
@@ -169,21 +197,12 @@ const SymptomEditView = ({ date, onClose, symptom, symptomData }) => {
|
||||
</Segment>
|
||||
)
|
||||
})}
|
||||
{/* show exclude AppSwitch for bleeding, mucus, cervix, temperature */}
|
||||
{/* but if fertility is off only for bleeding */}
|
||||
{shouldShow(symptomConfig.excludeText) &&
|
||||
(symptom === 'bleeding' || isFertilityTrackingEnabled) && (
|
||||
<Segment style={styles.segmentBorder}>
|
||||
<AppSwitch
|
||||
onToggle={onExcludeToggle}
|
||||
text={symtomPage[symptom].excludeText}
|
||||
value={data.exclude}
|
||||
/>
|
||||
</Segment>
|
||||
)}
|
||||
|
||||
{!isBleeding && excludeToggle}
|
||||
|
||||
{shouldShow(symptomConfig.note) && (
|
||||
<Segment style={styles.segmentBorder}>
|
||||
<AppText>{symtomPage[symptom].note}</AppText>
|
||||
<AppText>{symptomPage[symptom].note}</AppText>
|
||||
<AppTextInput
|
||||
{...inputProps}
|
||||
onChangeText={onEditNote}
|
||||
|
||||
@@ -23,6 +23,7 @@ const noteDescription = labels.noteExplainer
|
||||
const painLabels = labels.pain.categories
|
||||
const sexLabels = labels.sex.categories
|
||||
const temperatureLabels = labels.temperature
|
||||
const productLabels = labels.products.categories
|
||||
|
||||
const minutes = ChronoUnit.MINUTES
|
||||
|
||||
@@ -60,6 +61,15 @@ export const blank = {
|
||||
bleeding: {
|
||||
exclude: false,
|
||||
value: null,
|
||||
pad: null,
|
||||
tampon: null,
|
||||
underwear: null,
|
||||
cup: null,
|
||||
softTampon: null,
|
||||
disk: null,
|
||||
none: null,
|
||||
other: null,
|
||||
note: null,
|
||||
},
|
||||
cervix: {
|
||||
exclude: false,
|
||||
@@ -125,11 +135,10 @@ export const blank = {
|
||||
},
|
||||
}
|
||||
|
||||
export const symtomPage = {
|
||||
export const symptomPage = {
|
||||
bleeding: {
|
||||
excludeText: labels.bleeding.exclude.explainer,
|
||||
note: null,
|
||||
selectBoxGroups: null,
|
||||
selectTabGroups: [
|
||||
{
|
||||
key: 'value',
|
||||
@@ -137,6 +146,13 @@ export const symtomPage = {
|
||||
title: labels.bleeding.heaviness.explainer,
|
||||
},
|
||||
],
|
||||
selectBoxGroups: [
|
||||
{
|
||||
key: 'products',
|
||||
options: productLabels,
|
||||
title: labels.products.explainer,
|
||||
},
|
||||
],
|
||||
},
|
||||
cervix: {
|
||||
excludeText: cervixLabels.excludeExplainer,
|
||||
@@ -246,10 +262,49 @@ export const symtomPage = {
|
||||
|
||||
export const save = {
|
||||
bleeding: (data, date, shouldDeleteData) => {
|
||||
const { exclude, value } = data
|
||||
const isDataEntered = isNumber(value)
|
||||
const {
|
||||
value,
|
||||
pad,
|
||||
tampon,
|
||||
underwear,
|
||||
cup,
|
||||
softTampon,
|
||||
disk,
|
||||
none,
|
||||
other,
|
||||
note,
|
||||
} = data
|
||||
let { exclude } = data
|
||||
const isDataEntered = [
|
||||
'value',
|
||||
'pad',
|
||||
'tampon',
|
||||
'underwear',
|
||||
'cup',
|
||||
'softTampon',
|
||||
'disk',
|
||||
'none',
|
||||
'other',
|
||||
'note',
|
||||
].some((value) => typeof data[value] === 'number' || data[value] == true)
|
||||
// covering the case when a bleeding value was entered, "excluded" and saved. But then the bleeding value was deleted again.
|
||||
exclude = isNumber(value) ? exclude : false
|
||||
const valuesToSave =
|
||||
shouldDeleteData || !isDataEntered ? null : { value, exclude }
|
||||
shouldDeleteData || !isDataEntered
|
||||
? null
|
||||
: {
|
||||
exclude,
|
||||
value,
|
||||
pad,
|
||||
tampon,
|
||||
underwear,
|
||||
cup,
|
||||
softTampon,
|
||||
disk,
|
||||
none,
|
||||
other,
|
||||
note,
|
||||
}
|
||||
|
||||
saveSymptom('bleeding', date, valuesToSave)
|
||||
},
|
||||
@@ -329,10 +384,41 @@ const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => {
|
||||
}
|
||||
|
||||
const label = {
|
||||
bleeding: ({ value, exclude }) => {
|
||||
if (isNumber(value)) {
|
||||
const bleedingLabel = bleedingLabels[value]
|
||||
return exclude ? `(${bleedingLabel})` : bleedingLabel
|
||||
bleeding: (bleeding) => {
|
||||
bleeding = mapRealmObjToJsObj(bleeding)
|
||||
const bleedingLabel = []
|
||||
if (
|
||||
bleeding &&
|
||||
Object.values({ ...bleeding }).some(
|
||||
(val) => typeof val === 'number' || val == true
|
||||
)
|
||||
) {
|
||||
Object.keys(bleeding).forEach((key) => {
|
||||
if (bleeding[key] != null && key === 'value') {
|
||||
bleedingLabel.push(
|
||||
bleeding.exclude
|
||||
? `(${bleedingLabels[bleeding[key]]})`
|
||||
: bleedingLabels[bleeding[key]]
|
||||
)
|
||||
}
|
||||
if (
|
||||
bleeding[key] &&
|
||||
key !== 'other' &&
|
||||
key !== 'note' &&
|
||||
key !== 'value' &&
|
||||
key !== 'exclude'
|
||||
) {
|
||||
bleedingLabel.push(bleedingLabels[key] || productLabels[key])
|
||||
}
|
||||
if (key === 'other' && bleeding.other) {
|
||||
let label = productLabels[key]
|
||||
if (bleeding.note) {
|
||||
label = `${label} (${bleeding.note})`
|
||||
}
|
||||
bleedingLabel.push(label)
|
||||
}
|
||||
})
|
||||
return bleedingLabel.join(', ')
|
||||
}
|
||||
},
|
||||
temperature: ({ value, time, exclude }) => {
|
||||
|
||||
@@ -18,10 +18,6 @@ const AboutSection = () => {
|
||||
<AppPage title={t('title')}>
|
||||
<Segment>
|
||||
<AppText>{t('intro.text')}</AppText>
|
||||
<Button isCTA isSmall onPress={() => Linking.openURL(links.faq.url)}>
|
||||
{t('intro.faq')}
|
||||
</Button>
|
||||
<AppText>{t('intro.contact')}</AppText>
|
||||
<ButtonRow>
|
||||
{[links.email, links.gitlab, links.website].map((link) => (
|
||||
<Button
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
import Slider from '@ptomasroos/react-native-multi-slider'
|
||||
|
||||
import SliderLabel from './slider-label'
|
||||
import { styles } from './slider-styles'
|
||||
import {
|
||||
ADVANCE_PERIOD_NOTICE_DAYS_MIN,
|
||||
ADVANCE_PERIOD_NOTICE_DAYS_MAX,
|
||||
} from '../../../config'
|
||||
|
||||
const AdvanceNoticeDaysSlider = ({
|
||||
disabled,
|
||||
advanceNoticeDays,
|
||||
onAdvanceNoticeDaysChange,
|
||||
}) => {
|
||||
const sliderAccentBackground = disabled
|
||||
? styles.disabledSliderAccentBackground
|
||||
: styles.sliderAccentBackground
|
||||
|
||||
const sliderBackground = disabled
|
||||
? styles.disabledSliderBackground
|
||||
: styles.sliderBackground
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Slider
|
||||
customLabel={SliderLabel}
|
||||
enableLabel={true}
|
||||
markerStyle={styles.marker}
|
||||
markerOffsetY={styles.markerOffsetY}
|
||||
max={ADVANCE_PERIOD_NOTICE_DAYS_MAX}
|
||||
min={ADVANCE_PERIOD_NOTICE_DAYS_MIN}
|
||||
onValuesChange={onAdvanceNoticeDaysChange}
|
||||
step={1}
|
||||
showSteps={true}
|
||||
snapped={true}
|
||||
trackStyle={styles.slider}
|
||||
values={[advanceNoticeDays]}
|
||||
enabledOne={!disabled}
|
||||
enabledTwo={false}
|
||||
selectedStyle={sliderAccentBackground}
|
||||
unselectedStyle={sliderBackground}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdvanceNoticeDaysSlider
|
||||
|
||||
AdvanceNoticeDaysSlider.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
advanceNoticeDays: PropTypes.number,
|
||||
onAdvanceNoticeDaysChange: PropTypes.func,
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { Colors, Sizes } from '../../../styles'
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
paddingTop: Sizes.base,
|
||||
},
|
||||
marker: {
|
||||
backgroundColor: Colors.turquoiseDark,
|
||||
borderRadius: 50,
|
||||
elevation: 4,
|
||||
height: Sizes.subtitle,
|
||||
width: Sizes.subtitle,
|
||||
},
|
||||
slider: {
|
||||
borderRadius: 25,
|
||||
height: Sizes.small,
|
||||
paddingTop: Sizes.base,
|
||||
},
|
||||
sliderAccentBackground: {
|
||||
backgroundColor: Colors.turquoiseDark,
|
||||
},
|
||||
disabledSliderAccentBackground: {
|
||||
backgroundColor: Colors.grey,
|
||||
},
|
||||
sliderBackground: {
|
||||
backgroundColor: Colors.turquoise,
|
||||
},
|
||||
disabledSliderBackground: {
|
||||
backgroundColor: Colors.greyLight,
|
||||
},
|
||||
markerOffsetY: Sizes.tiny,
|
||||
})
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
import Slider from '@ptomasroos/react-native-multi-slider'
|
||||
|
||||
import SliderLabel from './slider-label'
|
||||
import { styles } from './slider-styles'
|
||||
import alertError from '../common/alert-error'
|
||||
import SliderLabel from './slider-label'
|
||||
|
||||
import { scaleObservable, saveTempScale } from '../../../local-storage'
|
||||
import { Colors, Sizes } from '../../../styles'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
import { TEMP_MIN, TEMP_MAX, TEMP_SLIDER_STEP } from '../../../config'
|
||||
|
||||
@@ -39,7 +40,7 @@ const TemperatureSlider = ({ disabled }) => {
|
||||
customLabel={SliderLabel}
|
||||
enableLabel={true}
|
||||
markerStyle={styles.marker}
|
||||
markerOffsetY={styles.markerOffsetY}
|
||||
markerOffsetY={Sizes.tiny}
|
||||
max={TEMP_MAX}
|
||||
min={TEMP_MIN}
|
||||
onValuesChange={onTemperatureSliderChange}
|
||||
@@ -60,3 +61,34 @@ export default TemperatureSlider
|
||||
TemperatureSlider.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
paddingTop: Sizes.base,
|
||||
},
|
||||
marker: {
|
||||
backgroundColor: Colors.turquoiseDark,
|
||||
|
||||
borderRadius: 50,
|
||||
elevation: 4,
|
||||
height: Sizes.subtitle,
|
||||
width: Sizes.subtitle,
|
||||
},
|
||||
slider: {
|
||||
borderRadius: 25,
|
||||
height: Sizes.small,
|
||||
},
|
||||
sliderAccentBackground: {
|
||||
backgroundColor: Colors.turquoiseDark,
|
||||
},
|
||||
disabledSliderAccentBackground: {
|
||||
backgroundColor: Colors.grey,
|
||||
},
|
||||
sliderBackground: {
|
||||
backgroundColor: Colors.turquoise,
|
||||
},
|
||||
disabledSliderBackground: {
|
||||
backgroundColor: Colors.greyLight,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import AppSwitch from '../../common/app-switch'
|
||||
import AdvanceNoticeDaysSlider from '../customization/advance-notice-days-slider'
|
||||
|
||||
import {
|
||||
periodReminderObservable,
|
||||
savePeriodReminder,
|
||||
periodPredictionObservable,
|
||||
saveAdvanceNoticeDays,
|
||||
advanceNoticeDaysObservable,
|
||||
} from '../../../local-storage'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
|
||||
const PeriodReminder = () => {
|
||||
const isPeriodPredictionDisabled = !periodPredictionObservable.value
|
||||
|
||||
const [isPeriodReminderEnabled, setIsPeriodReminderEnabled] = useState(
|
||||
periodReminderObservable.value.enabled
|
||||
)
|
||||
|
||||
const [advanceNoticeDays, setAdvanceNoticeDays] = useState(
|
||||
advanceNoticeDaysObservable.value
|
||||
)
|
||||
|
||||
const periodReminderToggle = (isEnabled) => {
|
||||
setIsPeriodReminderEnabled(isEnabled)
|
||||
savePeriodReminder({ enabled: isEnabled })
|
||||
}
|
||||
|
||||
const handleAdvanceNoticeDaysChange = (days) => {
|
||||
setAdvanceNoticeDays(days)
|
||||
saveAdvanceNoticeDays(days)
|
||||
}
|
||||
|
||||
const reminderText =
|
||||
advanceNoticeDays == 1
|
||||
? labels.periodReminder.reminderTextSingular
|
||||
: labels.periodReminder.reminderTextPlural(advanceNoticeDays)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppSwitch
|
||||
onToggle={periodReminderToggle}
|
||||
text={reminderText}
|
||||
value={isPeriodReminderEnabled}
|
||||
disabled={isPeriodPredictionDisabled}
|
||||
/>
|
||||
{isPeriodReminderEnabled && (
|
||||
<AdvanceNoticeDaysSlider
|
||||
disabled={isPeriodPredictionDisabled}
|
||||
advanceNoticeDays={parseInt(advanceNoticeDays)}
|
||||
onAdvanceNoticeDaysChange={handleAdvanceNoticeDaysChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PeriodReminder
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import AppPage from '../../common/app-page'
|
||||
import AppSwitch from '../../common/app-switch'
|
||||
import Segment from '../../common/segment'
|
||||
import TemperatureReminder from './temperature-reminder'
|
||||
import PeriodReminder from './period-reminder'
|
||||
|
||||
import {
|
||||
periodReminderObservable,
|
||||
savePeriodReminder,
|
||||
periodPredictionObservable,
|
||||
temperatureTrackingCategoryObservable,
|
||||
} from '../../../local-storage'
|
||||
@@ -14,7 +16,17 @@ import labels from '../../../i18n/en/settings'
|
||||
import { Alert, Pressable } from 'react-native'
|
||||
|
||||
const Reminders = () => {
|
||||
const periodReminderDisabledPrompt = () => {
|
||||
const isPeriodPredictionDisabled = !periodPredictionObservable.value
|
||||
|
||||
const [isPeriodReminderEnabled, setIsPeriodReminderEnabled] = useState(
|
||||
periodReminderObservable.value.enabled
|
||||
)
|
||||
const periodReminderToggle = (isEnabled) => {
|
||||
setIsPeriodReminderEnabled(isEnabled)
|
||||
savePeriodReminder({ enabled: isEnabled })
|
||||
}
|
||||
|
||||
const reminderDisabledPrompt = () => {
|
||||
if (!periodPredictionObservable.value) {
|
||||
Alert.alert(
|
||||
labels.periodReminder.alertNoPeriodReminder.title,
|
||||
@@ -31,12 +43,16 @@ const Reminders = () => {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppPage>
|
||||
<Pressable onPress={periodReminderDisabledPrompt}>
|
||||
<Pressable onPress={reminderDisabledPrompt}>
|
||||
<Segment title={labels.periodReminder.title}>
|
||||
<PeriodReminder />
|
||||
<AppSwitch
|
||||
onToggle={periodReminderToggle}
|
||||
text={labels.periodReminder.reminderText}
|
||||
value={isPeriodReminderEnabled}
|
||||
disabled={isPeriodPredictionDisabled}
|
||||
/>
|
||||
</Segment>
|
||||
</Pressable>
|
||||
<Pressable onPress={tempReminderDisabledPrompt}>
|
||||
|
||||
@@ -33,10 +33,6 @@ export const TEMP_MAX = 39
|
||||
export const TEMP_MIN = 35
|
||||
export const TEMP_SLIDER_STEP = 0.5
|
||||
|
||||
export const ADVANCE_PERIOD_NOTICE_DAYS_MIN = 1
|
||||
export const ADVANCE_PERIOD_NOTICE_DAYS_MAX = 7
|
||||
export const ADVANCE_PERIOD_NOTICE_DAYS_INIT_VALUE = 3
|
||||
|
||||
export const HIT_SLOP = {
|
||||
top: verticalScale(20),
|
||||
bottom: verticalScale(20),
|
||||
|
||||
+1
-1
@@ -65,6 +65,7 @@ export function getBleedingDaysSortedByDate() {
|
||||
return db
|
||||
.objects('CycleDay')
|
||||
.filtered('bleeding != null')
|
||||
.filtered('bleeding.value != null')
|
||||
.sorted('date', true)
|
||||
}
|
||||
export function getTemperatureDaysSortedByDate() {
|
||||
@@ -88,7 +89,6 @@ export function getCycleStartsSortedByDate() {
|
||||
export function saveSymptom(symptom, date, val) {
|
||||
let cycleDay = getCycleDay(date)
|
||||
if (!cycleDay) cycleDay = createCycleDay(date)
|
||||
|
||||
db.write(() => {
|
||||
if (symptom === 'bleeding') {
|
||||
const mensesDaysAfter = getMensesDaysRightAfter(cycleDay)
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
const TemperatureSchema = {
|
||||
name: 'Temperature',
|
||||
properties: {
|
||||
value: 'double',
|
||||
exclude: 'bool',
|
||||
time: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const BleedingSchema = {
|
||||
name: 'Bleeding',
|
||||
properties: {
|
||||
value: { type: 'int', optional: true },
|
||||
exclude: 'bool',
|
||||
pad: { type: 'bool', optional: true },
|
||||
tampon: { type: 'bool', optional: true },
|
||||
underwear: { type: 'bool', optional: true },
|
||||
cup: { type: 'bool', optional: true },
|
||||
softTampon: { type: 'bool', optional: true },
|
||||
disk: { type: 'bool', optional: true },
|
||||
none: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true },
|
||||
},
|
||||
}
|
||||
|
||||
const MucusSchema = {
|
||||
name: 'Mucus',
|
||||
properties: {
|
||||
feeling: { type: 'int', optional: true },
|
||||
texture: { type: 'int', optional: true },
|
||||
value: { type: 'int', optional: true },
|
||||
exclude: 'bool',
|
||||
},
|
||||
}
|
||||
|
||||
const CervixSchema = {
|
||||
name: 'Cervix',
|
||||
properties: {
|
||||
opening: { type: 'int', optional: true },
|
||||
firmness: { type: 'int', optional: true },
|
||||
position: { type: 'int', optional: true },
|
||||
exclude: 'bool',
|
||||
},
|
||||
}
|
||||
|
||||
const NoteSchema = {
|
||||
name: 'Note',
|
||||
properties: {
|
||||
value: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
const DesireSchema = {
|
||||
name: 'Desire',
|
||||
properties: {
|
||||
value: 'int',
|
||||
},
|
||||
}
|
||||
|
||||
const SexSchema = {
|
||||
name: 'Sex',
|
||||
properties: {
|
||||
solo: { type: 'bool', optional: true },
|
||||
partner: { type: 'bool', optional: true },
|
||||
condom: { type: 'bool', optional: true },
|
||||
pill: { type: 'bool', optional: true },
|
||||
iud: { type: 'bool', optional: true },
|
||||
patch: { type: 'bool', optional: true },
|
||||
ring: { type: 'bool', optional: true },
|
||||
implant: { type: 'bool', optional: true },
|
||||
diaphragm: { type: 'bool', optional: true },
|
||||
none: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true },
|
||||
},
|
||||
}
|
||||
|
||||
const PainSchema = {
|
||||
name: 'Pain',
|
||||
properties: {
|
||||
cramps: { type: 'bool', optional: true },
|
||||
ovulationPain: { type: 'bool', optional: true },
|
||||
headache: { type: 'bool', optional: true },
|
||||
backache: { type: 'bool', optional: true },
|
||||
nausea: { type: 'bool', optional: true },
|
||||
tenderBreasts: { type: 'bool', optional: true },
|
||||
migraine: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true },
|
||||
},
|
||||
}
|
||||
|
||||
const MoodSchema = {
|
||||
name: 'Mood',
|
||||
properties: {
|
||||
happy: { type: 'bool', optional: true },
|
||||
sad: { type: 'bool', optional: true },
|
||||
stressed: { type: 'bool', optional: true },
|
||||
balanced: { type: 'bool', optional: true },
|
||||
fine: { type: 'bool', optional: true },
|
||||
anxious: { type: 'bool', optional: true },
|
||||
energetic: { type: 'bool', optional: true },
|
||||
fatigue: { type: 'bool', optional: true },
|
||||
angry: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true },
|
||||
},
|
||||
}
|
||||
|
||||
const CycleDaySchema = {
|
||||
name: 'CycleDay',
|
||||
primaryKey: 'date',
|
||||
properties: {
|
||||
date: 'string',
|
||||
temperature: {
|
||||
type: 'Temperature',
|
||||
optional: true,
|
||||
},
|
||||
isCycleStart: 'bool',
|
||||
bleeding: {
|
||||
type: 'Bleeding',
|
||||
optional: true,
|
||||
},
|
||||
mucus: {
|
||||
type: 'Mucus',
|
||||
optional: true,
|
||||
},
|
||||
cervix: {
|
||||
type: 'Cervix',
|
||||
optional: true,
|
||||
},
|
||||
note: {
|
||||
type: 'Note',
|
||||
optional: true,
|
||||
},
|
||||
desire: {
|
||||
type: 'Desire',
|
||||
optional: true,
|
||||
},
|
||||
sex: {
|
||||
type: 'Sex',
|
||||
optional: true,
|
||||
},
|
||||
pain: {
|
||||
type: 'Pain',
|
||||
optional: true,
|
||||
},
|
||||
mood: {
|
||||
type: 'Mood',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
schema: [
|
||||
CycleDaySchema,
|
||||
TemperatureSchema,
|
||||
BleedingSchema,
|
||||
MucusSchema,
|
||||
CervixSchema,
|
||||
NoteSchema,
|
||||
DesireSchema,
|
||||
SexSchema,
|
||||
PainSchema,
|
||||
MoodSchema,
|
||||
],
|
||||
schemaVersion: 5,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion < 5) {
|
||||
const newObjects = newRealm.objects('Bleeding')
|
||||
|
||||
// loop through all objects and assign a default value for new properties
|
||||
for (let i = 0; i < newObjects.length; i++) {
|
||||
newObjects[i].pad = false
|
||||
newObjects[i].tampon = false
|
||||
newObjects[i].underwear = false
|
||||
newObjects[i].cup = false
|
||||
newObjects[i].softTampon = false
|
||||
newObjects[i].disk = false
|
||||
newObjects[i].none = false
|
||||
newObjects[i].other = false
|
||||
newObjects[i].note = null
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
+2
-1
@@ -3,5 +3,6 @@ import schema1 from './1.js'
|
||||
import schema2 from './2.js'
|
||||
import schema3 from './3.js'
|
||||
import schema4 from './4.js'
|
||||
import schema5 from './5.js'
|
||||
|
||||
export default [schema0, schema1, schema2, schema3, schema4]
|
||||
export default [schema0, schema1, schema2, schema3, schema4, schema5]
|
||||
|
||||
+1
-3
@@ -45,9 +45,7 @@
|
||||
"text": "The drips are developing this app on a volunteer basis. We are always grateful for support. This could mean condriputing to the code, giving feedback, suggesting improvements or features, testing or donating. It helps and motivates us maintaining this app and developing new features. Thank you for your support!"
|
||||
},
|
||||
"intro": {
|
||||
"text": "Please note that your data is stored locally on your phone and not on a server. This means your data cannot be read by anyone else unless they have access to your phone. We want to ensure that you stay in control of your own data. If you are planning to switch or reset your phone, please remember to export your data before doing so. You can reinstall the app afterwards and import your data.\n\nIf you encounter any issues, please take a look at our Frequently Asked Questions page.",
|
||||
"faq": "FAQ",
|
||||
"contact": "\nIf your issue is not listed, don't hesitate to contact us via email. You can also contribute to the code base on Gitlab and visit our website."
|
||||
"text": "Please note that your data is stored locally on your phone and not on a server. This means your data cannot be read by anyone else unless they have access to your phone. We want to ensure that you stay in control of your own data. If you are planning to switch or reset your phone, please remember to export your data before doing so. You can reinstall the app afterwards and import your data.\n\nIf you encounter any technical issues, don't hesitate to contact us via email. You can also contribute to the code base on Gitlab and visit our website."
|
||||
},
|
||||
"philosophy": {
|
||||
"title": "Remember to think for yourself",
|
||||
|
||||
+17
-2
@@ -13,6 +13,21 @@ export const bleeding = {
|
||||
},
|
||||
}
|
||||
|
||||
export const products = {
|
||||
categories: {
|
||||
pad: 'pad',
|
||||
tampon: 'tampon',
|
||||
underwear: 'underwear',
|
||||
cup: 'cup',
|
||||
softTampon: 'soft tampon',
|
||||
disk: 'disk',
|
||||
none: 'none',
|
||||
other: 'other',
|
||||
},
|
||||
header: 'period products',
|
||||
explainer: 'Did you use period products?',
|
||||
}
|
||||
|
||||
export const cervix = {
|
||||
subcategories: {
|
||||
opening: 'opening',
|
||||
@@ -32,7 +47,7 @@ export const 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.",
|
||||
"You can exclude these values if you don't want to use them for fertility detection.",
|
||||
}
|
||||
|
||||
export const mucus = {
|
||||
@@ -50,7 +65,7 @@ export const mucus = {
|
||||
'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",
|
||||
"You can exclude these values if you don't want to use them for fertility detection",
|
||||
}
|
||||
|
||||
export const desire = {
|
||||
|
||||
@@ -39,8 +39,4 @@ export default {
|
||||
url: 'https://www.flaticon.com',
|
||||
text: 'Flaticon',
|
||||
},
|
||||
faq: {
|
||||
url: 'https://dripapp.org/faq',
|
||||
text: 'FAQ',
|
||||
},
|
||||
}
|
||||
|
||||
+4
-6
@@ -60,12 +60,10 @@ export default {
|
||||
},
|
||||
periodReminder: {
|
||||
title: 'Next period reminder',
|
||||
reminderTextSingular:
|
||||
'Get a notification 1 day before your next period is likely to start.',
|
||||
reminderTextPlural: (days) =>
|
||||
`Get a notification ${days} days before your next period is likely to start.`,
|
||||
notification: (advanceNoticeDays, daysToEndOfPrediction) =>
|
||||
`Your next period is likely to start in ${advanceNoticeDays} to ${daysToEndOfPrediction} days.`,
|
||||
reminderText:
|
||||
'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:
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2410.22</string>
|
||||
<string>1.2409.26</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
+25
-55
@@ -2,7 +2,6 @@ import { Platform } from 'react-native'
|
||||
import {
|
||||
tempReminderObservable,
|
||||
periodReminderObservable,
|
||||
advanceNoticeDaysObservable,
|
||||
} from '../local-storage'
|
||||
import * as PN from 'react-native-push-notification'
|
||||
import { requestNotifications } from 'react-native-permissions'
|
||||
@@ -14,19 +13,12 @@ import { getBleedingDaysSortedByDate } from '../db'
|
||||
import cycleModule from './cycle'
|
||||
import nothingChanged from '../db/db-unchanged'
|
||||
|
||||
const DRIP_CHANNEL_ID = 'drip-channel-id'
|
||||
const TEMPERATURE_REMINDER_ID = '1'
|
||||
const PERIOD_REMINDER_ID = '2'
|
||||
const PushNotification = Platform.OS === 'ios' ? PN : PN.default
|
||||
|
||||
export default function setupNotifications(navigate, setDate) {
|
||||
// for Android, this method call is necessary
|
||||
if (Platform.OS === 'android') {
|
||||
requestNotifications()
|
||||
}
|
||||
Platform.OS === 'android' ? requestNotifications() : null
|
||||
const PushNotification = Platform.OS === 'ios' ? PN : PN.default
|
||||
|
||||
PushNotification.createChannel({
|
||||
channelId: DRIP_CHANNEL_ID, // (required)
|
||||
channelId: 'drip-channel-id', // (required)
|
||||
channelName: 'drip reminder', // (required)
|
||||
playSound: false, // (optional) default: true
|
||||
})
|
||||
@@ -34,10 +26,7 @@ export default function setupNotifications(navigate, setDate) {
|
||||
PushNotification.configure({
|
||||
onNotification: (notification) => {
|
||||
// https://github.com/zo0r/react-native-push-notification/issues/966#issuecomment-479069106
|
||||
if (
|
||||
notification.data?.id === TEMPERATURE_REMINDER_ID ||
|
||||
notification.id === TEMPERATURE_REMINDER_ID
|
||||
) {
|
||||
if (notification.data?.id === '1' || notification.id === '1') {
|
||||
const todayDate = LocalDate.now().toString()
|
||||
setDate(todayDate)
|
||||
navigate('TemperatureEditView')
|
||||
@@ -48,7 +37,7 @@ export default function setupNotifications(navigate, setDate) {
|
||||
})
|
||||
|
||||
tempReminderObservable((reminder) => {
|
||||
PushNotification.cancelLocalNotification({ id: TEMPERATURE_REMINDER_ID })
|
||||
PushNotification.cancelLocalNotification({ id: '1' })
|
||||
if (reminder.enabled) {
|
||||
const [hours, minutes] = reminder.time.split(':')
|
||||
let target = new Moment()
|
||||
@@ -61,75 +50,56 @@ export default function setupNotifications(navigate, setDate) {
|
||||
}
|
||||
|
||||
PushNotification.localNotificationSchedule({
|
||||
id: TEMPERATURE_REMINDER_ID,
|
||||
userInfo: { id: TEMPERATURE_REMINDER_ID },
|
||||
id: '1',
|
||||
userInfo: { id: '1' },
|
||||
message: labels.tempReminder.notification,
|
||||
date: target.toDate(),
|
||||
vibrate: false,
|
||||
repeatType: 'day',
|
||||
channelId: DRIP_CHANNEL_ID,
|
||||
allowWhileIdle: true,
|
||||
channelId: 'drip-channel-id',
|
||||
})
|
||||
}
|
||||
}, false)
|
||||
|
||||
periodReminderObservable(() => updatePeriodNotification(), false)
|
||||
advanceNoticeDaysObservable(() => updatePeriodNotification(), false)
|
||||
periodReminderObservable((reminder) => {
|
||||
PushNotification.cancelLocalNotification({ id: '2' })
|
||||
if (reminder.enabled) setupPeriodReminder()
|
||||
}, false)
|
||||
|
||||
getBleedingDaysSortedByDate().addListener((_, changes) => {
|
||||
// the listener fires on setup, so we check if there were actually any changes
|
||||
if (nothingChanged(changes)) {
|
||||
return
|
||||
}
|
||||
|
||||
updatePeriodNotification()
|
||||
if (nothingChanged(changes)) return
|
||||
PushNotification.cancelLocalNotification({ id: '2' })
|
||||
if (periodReminderObservable.value.enabled) setupPeriodReminder()
|
||||
})
|
||||
}
|
||||
|
||||
const updatePeriodNotification = () => {
|
||||
// Cancel any existing period reminder
|
||||
PushNotification.cancelLocalNotification({ id: PERIOD_REMINDER_ID })
|
||||
|
||||
// Set up a new period reminder if enabled
|
||||
if (periodReminderObservable.value.enabled) {
|
||||
schedulePeriodNotification()
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePeriodNotification() {
|
||||
function setupPeriodReminder() {
|
||||
const PushNotification = Platform.OS === 'ios' ? PN : PN.default
|
||||
const bleedingPrediction = cycleModule().getPredictedMenses()
|
||||
|
||||
if (bleedingPrediction.length > 0) {
|
||||
const predictedBleedingStart = Moment(
|
||||
bleedingPrediction[0][0],
|
||||
'YYYY-MM-DD'
|
||||
)
|
||||
|
||||
const advanceNoticeDays = parseInt(advanceNoticeDaysObservable.value)
|
||||
|
||||
// ${advanceNoticeDays} days before and at 6 am
|
||||
// 3 days before and at 6 am
|
||||
const reminderDate = predictedBleedingStart
|
||||
.subtract(advanceNoticeDays, 'days')
|
||||
.subtract(3, 'days')
|
||||
.hours(6)
|
||||
.minutes(0)
|
||||
.seconds(0)
|
||||
|
||||
if (reminderDate.isAfter()) {
|
||||
// period is likely to start in advanceNoticeDays to advanceNoticeDays + (length of prediction - 1) days
|
||||
const daysToEndOfPrediction =
|
||||
advanceNoticeDays + bleedingPrediction[0].length - 1
|
||||
// period is likely to start in 3 to 3 + (length of prediction - 1) days
|
||||
const daysToEndOfPrediction = bleedingPrediction[0].length + 2
|
||||
|
||||
PushNotification.localNotificationSchedule({
|
||||
id: PERIOD_REMINDER_ID,
|
||||
userInfo: { id: PERIOD_REMINDER_ID },
|
||||
message: labels.periodReminder.notification(
|
||||
advanceNoticeDays,
|
||||
daysToEndOfPrediction
|
||||
),
|
||||
id: '2',
|
||||
userInfo: { id: '2' },
|
||||
message: labels.periodReminder.notification(daysToEndOfPrediction),
|
||||
date: reminderDate.toDate(),
|
||||
vibrate: false,
|
||||
channelId: DRIP_CHANNEL_ID,
|
||||
allowWhileIdle: true,
|
||||
channelId: 'drip-channel-id',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export default function ({
|
||||
val, cycleDay, mensesDaysAfter, checkIsMensesStart
|
||||
val,
|
||||
cycleDay,
|
||||
mensesDaysAfter,
|
||||
checkIsMensesStart,
|
||||
}) {
|
||||
|
||||
cycleDay.bleeding = val
|
||||
|
||||
// if a bleeding value is deleted or excluded, we need to check if there are
|
||||
@@ -20,15 +22,15 @@ export default function ({
|
||||
}
|
||||
|
||||
function bleedingValueDeletedOrExluded(val) {
|
||||
const bleedingDeleted = !val
|
||||
const bleedingDeleted = !val || !(typeof val?.value === 'number')
|
||||
const bleedingExcluded = val && val.exclude
|
||||
return bleedingDeleted || bleedingExcluded
|
||||
}
|
||||
|
||||
function maybeClearOldCycleStarts() {
|
||||
// if we have a new bleeding day, we need to clear the
|
||||
// menses start marker from all following days of this
|
||||
// menses that may have been marked as start before
|
||||
mensesDaysAfter.forEach(day => day.isCycleStart = false)
|
||||
// if we have a new bleeding day, we need to clear the
|
||||
// menses start marker from all following days of this
|
||||
// menses that may have been marked as start before
|
||||
mensesDaysAfter.forEach((day) => (day.isCycleStart = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-15
@@ -2,8 +2,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import Observable from 'obv'
|
||||
import { TEMP_SCALE_MIN, TEMP_SCALE_MAX, TEMP_SCALE_UNITS } from './config'
|
||||
|
||||
import { ADVANCE_PERIOD_NOTICE_DAYS_INIT_VALUE } from './config'
|
||||
|
||||
export const scaleObservable = Observable()
|
||||
setObvWithInitValue('tempScale', scaleObservable, {
|
||||
min: TEMP_SCALE_MIN,
|
||||
@@ -61,18 +59,6 @@ export async function savePeriodPrediction(bool) {
|
||||
}
|
||||
}
|
||||
|
||||
export const advanceNoticeDaysObservable = Observable()
|
||||
setObvWithInitValue(
|
||||
'advanceNoticeDays',
|
||||
advanceNoticeDaysObservable,
|
||||
parseInt(ADVANCE_PERIOD_NOTICE_DAYS_INIT_VALUE)
|
||||
)
|
||||
|
||||
export async function saveAdvanceNoticeDays(days) {
|
||||
await AsyncStorage.setItem('advanceNoticeDays', JSON.stringify(days))
|
||||
advanceNoticeDaysObservable.set(days)
|
||||
}
|
||||
|
||||
export const useCervixAsSecondarySymptomObservable = Observable()
|
||||
setObvWithInitValue(
|
||||
'useCervixAsSecondarySymptom',
|
||||
@@ -123,7 +109,7 @@ export async function saveTemperatureTrackingCategory(bool) {
|
||||
if (!temperatureTrackingCategoryObservable.value) {
|
||||
// if temperature tracking is turned off, the temperature reminder gets disabled
|
||||
const tempReminderResult = await AsyncStorage.getItem('tempReminder')
|
||||
if (tempReminderResult && JSON.parse(tempReminderResult).enabled) {
|
||||
if (tempReminderResult && JSON.parse(tempReminderResult).enabled) {
|
||||
tempReminderObservable.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "drip.",
|
||||
"version": "1.2410.22",
|
||||
"version": "1.2409.26",
|
||||
"contributors": [
|
||||
"Julia Friesel <julia.friesel@gmail.com>",
|
||||
"Marie Kochsiek",
|
||||
"Tina Baumann",
|
||||
"Sofiya Tepikin",
|
||||
"Mariya Zadnepryanets",
|
||||
"Lisa Hillebrand",
|
||||
"Martha Dörfler"
|
||||
"Lisa Hillebrand"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
@@ -48,7 +47,7 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-native": "0.68.5",
|
||||
"react-native": "0.68.3",
|
||||
"react-native-calendars": "^1.1287.0",
|
||||
"react-native-document-picker": "^8.1.1",
|
||||
"react-native-fs": "^2.20.0",
|
||||
|
||||
@@ -6378,7 +6378,7 @@ promise@^7.1.1:
|
||||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
promise@^8.2.0:
|
||||
promise@^8.0.3:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a"
|
||||
integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==
|
||||
@@ -6508,10 +6508,10 @@ react-native-calendars@^1.1287.0:
|
||||
optionalDependencies:
|
||||
moment "^2.29.4"
|
||||
|
||||
react-native-codegen@^0.0.18:
|
||||
version "0.0.18"
|
||||
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.18.tgz#99d6623d65292e8ce3fdb1d133a358caaa2145e7"
|
||||
integrity sha512-XPI9aVsFy3dvgDZvyGWrFnknNiyb22kg5nHgxa0vjWTH9ENLBgVRZt9A64xHZ8BYihH+gl0p/1JNOCIEUzRPBg==
|
||||
react-native-codegen@^0.0.17:
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.17.tgz#83fb814d94061cbd46667f510d2ddba35ffb50ac"
|
||||
integrity sha512-7GIEUmAemH9uWwB6iYXNNsPoPgH06pxzGRmdBzK98TgFBdYJZ7CBuZFPMe4jmHQTPOkQazKZ/w5O6/71JBixmw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.14.0"
|
||||
flow-parser "^0.121.0"
|
||||
@@ -6601,10 +6601,10 @@ react-native-version@^3.1.0:
|
||||
resolve-from "^5.0.0"
|
||||
semver "^6.0.0"
|
||||
|
||||
react-native@0.68.5:
|
||||
version "0.68.5"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.68.5.tgz#8ba7389e00b757c59b6ea23bf38303d52367d155"
|
||||
integrity sha512-t3kiQ/gumFV+0r/NRSIGtYxanjY4da0utFqHgkMcRPJVwXFWC0Fr8YiOeRGYO1dp8EfrSsOjtfWic/inqVYlbQ==
|
||||
react-native@0.68.3:
|
||||
version "0.68.3"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.68.3.tgz#07ac7374acde9bc5e80f9e473e03d6b730528f1c"
|
||||
integrity sha512-LPgLQ4e96NWCrJPKlXzKfvlg1ddhfUplsEg00/cfBIMFZPJn2inzo9Rym8I/JYjmRORe4GjGY8kOem72hPm0Lw==
|
||||
dependencies:
|
||||
"@jest/create-cache-key-function" "^27.0.1"
|
||||
"@react-native-community/cli" "^7.0.3"
|
||||
@@ -6626,9 +6626,9 @@ react-native@0.68.5:
|
||||
metro-source-map "0.67.0"
|
||||
nullthrows "^1.1.1"
|
||||
pretty-format "^26.5.2"
|
||||
promise "^8.2.0"
|
||||
promise "^8.0.3"
|
||||
react-devtools-core "^4.23.0"
|
||||
react-native-codegen "^0.0.18"
|
||||
react-native-codegen "^0.0.17"
|
||||
react-native-gradle-plugin "^0.0.6"
|
||||
react-refresh "^0.4.0"
|
||||
react-shallow-renderer "16.14.1"
|
||||
|
||||
Reference in New Issue
Block a user