├── fastlane └── metadata │ └── android │ └── en-US │ ├── title.txt │ ├── changelogs │ ├── 27.txt │ ├── 1.txt │ ├── 26.txt │ ├── 18.txt │ ├── 32.txt │ ├── 37.txt │ ├── 45.txt │ ├── 16.txt │ ├── 19.txt │ ├── 4.txt │ ├── 5.txt │ ├── 39.txt │ ├── 38.txt │ ├── 10.txt │ ├── 12.txt │ ├── 40.txt │ ├── 41.txt │ ├── 43.txt │ ├── 21.txt │ ├── 42.txt │ ├── 44.txt │ ├── 6.txt │ ├── 35.txt │ ├── 15.txt │ ├── 20.txt │ ├── 14.txt │ ├── 13.txt │ ├── 8.txt │ ├── 11.txt │ ├── 28.txt │ ├── 3.txt │ ├── 24.txt │ ├── 34.txt │ ├── 17.txt │ ├── 22.txt │ ├── 31.txt │ ├── 33.txt │ ├── 9.txt │ └── 36.txt │ ├── short_description.txt │ ├── images │ ├── icon.png │ ├── featureGraphic.png │ └── phoneScreenshots │ │ └── p1.png │ └── full_description.txt ├── images.d.ts ├── assets ├── icon.png ├── splash.png ├── favicon.png ├── icon-round.png ├── adaptive-icon.png ├── monochrome-icon.png ├── images │ ├── LICENSE │ └── search-empty.svg ├── onboarding │ ├── LICENSE │ └── description.svg ├── data │ └── demo.ts ├── monochrome-icon.svg ├── adaptive-icon.svg └── styles │ └── index.ts ├── i18n └── index.ts ├── scripts ├── check.sh ├── xml │ └── colors.xml ├── android.sh ├── staging-web │ └── build.sh ├── production-web │ └── build.sh ├── update.sh ├── f-droid.sh └── patches │ └── README.md ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yml ├── declarations.d.ts ├── serve.json ├── babel.config.js ├── components ├── Icon.tsx ├── index.ts ├── Filters.tsx ├── TabBarIcon.tsx ├── VerticalView.tsx ├── Alert.tsx ├── Message.tsx ├── CardItemDonate.tsx ├── CardItemLikes.tsx ├── InterestModal.tsx ├── ComplimentModal.tsx ├── SelectModal.tsx ├── AgeRangeSliderModal.tsx ├── InterestView.tsx ├── ColorModal.tsx └── CardItemSearch.tsx ├── index.js ├── screens ├── profile │ ├── index.ts │ ├── Settings.tsx │ ├── AdvancedSettings.tsx │ ├── Pictures.tsx │ ├── Prompts.tsx │ └── SearchSettings.tsx ├── index.ts ├── Messages.tsx ├── Donate.tsx ├── MessageDetail.tsx ├── PasswordReset.tsx ├── Main.tsx └── Likes.tsx ├── tsconfig.json ├── patches ├── README.md ├── react-native-swiper-flatlist+3.2.5.patch ├── patch-package+8.0.0.patch └── react-native-card-stack-swiper+1.2.5.patch ├── i18n.tsx ├── plugins ├── setClearTextTrafficFalse.js └── withGradleProperties.js ├── metro.config.js ├── eas.json ├── LICENSE_old ├── .gitignore ├── README.md ├── app.config.js ├── package.json ├── URL.tsx └── Global.tsx /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Alovoa -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/27.txt: -------------------------------------------------------------------------------- 1 | More fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Initial release 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/26.txt: -------------------------------------------------------------------------------- 1 | Fixed numerous bugs -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg"; 2 | declare module "*.png"; 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/18.txt: -------------------------------------------------------------------------------- 1 | Updated dependencies -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/32.txt: -------------------------------------------------------------------------------- 1 | Several UI and bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/37.txt: -------------------------------------------------------------------------------- 1 | Fix search settings 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/45.txt: -------------------------------------------------------------------------------- 1 | Update to Expo 54.0.27 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | UI improvements and bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/19.txt: -------------------------------------------------------------------------------- 1 | Fixed several critical bugs -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | - Users can now report other users -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/assets/splash.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Users can now login with email address -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon-round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/assets/icon-round.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/39.txt: -------------------------------------------------------------------------------- 1 | Fixed dark mode issues 2 | Minor UI changes 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Open-source online dating application 2 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/38.txt: -------------------------------------------------------------------------------- 1 | Update Expo to version 52 2 | Smaller UI fixes 3 | -------------------------------------------------------------------------------- /assets/monochrome-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/assets/monochrome-icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | Fix app force closing 2 | Improve display of user images -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/12.txt: -------------------------------------------------------------------------------- 1 | Fix display issues on some pages 2 | Update dependencies -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/40.txt: -------------------------------------------------------------------------------- 1 | Fix issue with Search page displaying hidden/liked users -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/41.txt: -------------------------------------------------------------------------------- 1 | Fix issue distance slide behaving strangely on Android 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/43.txt: -------------------------------------------------------------------------------- 1 | Show notification dots in profile if it's incomplete 2 | -------------------------------------------------------------------------------- /i18n/index.ts: -------------------------------------------------------------------------------- 1 | import de from './de.json'; 2 | import en from './en.json'; 3 | export default { de, en }; 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21.txt: -------------------------------------------------------------------------------- 1 | Fix duplicate characters when complimenting 2 | General UI fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/42.txt: -------------------------------------------------------------------------------- 1 | Fix screens being forced to reload after certain actions 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/44.txt: -------------------------------------------------------------------------------- 1 | Update to Expo 54 2 | Fix virtual keyboard hiding input fields -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Users can now register with email address 2 | Several bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/35.txt: -------------------------------------------------------------------------------- 1 | Improve onboarding process 2 | Bug fixes and general improvements -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | New and improved UI, now supports all kind of devices and screen sizes :) -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | You can now send a compliment to any compatible person 2 | Improved UI -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | You can now see your liked, hidden and blocked users. 2 | Visual improvements. 3 | -------------------------------------------------------------------------------- /scripts/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | yarn type-check 3 | yarn lint --fix 4 | EXPO_DOCTOR_ENABLE_DIRECTORY_CHECK=false yarn doctor 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | New and improved UI and elements 2 | Quality of life improvements 3 | Lots of bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | Fix preferred gender switches in profile. 2 | Fix Donate UI. 3 | Improve image processing. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | Fix login issues 2 | Fix location issues for F-droid build 3 | Upgrade Expo SDK to version 49 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | module.exports = { 3 | extends: 'expo', 4 | ignorePatterns: ['/dist*'], 5 | }; 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/28.txt: -------------------------------------------------------------------------------- 1 | Improve reporting process of users 2 | More efficient handling of data 3 | General bug fixes and improvements -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | - Updated splash screen 2 | - User can now view and upload additional photos 3 | - Several UI improvements -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/24.txt: -------------------------------------------------------------------------------- 1 | Added profile prompts 2 | You can now receive email notifications on likes and chats 3 | General UI and bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/34.txt: -------------------------------------------------------------------------------- 1 | You can now double tap on images of a person on their profile to zoom in. 2 | General UI improvements and bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/17.txt: -------------------------------------------------------------------------------- 1 | New and improved profile layout 2 | You can now customize the colors within the app 3 | More bug fixes and improvements 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/p1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alovoa/alovoa-expo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/p1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/22.txt: -------------------------------------------------------------------------------- 1 | Improve registering and onboarding process 2 | Added referral codes that you can share to your friends 3 | General UI and bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/31.txt: -------------------------------------------------------------------------------- 1 | Improve reporting process of users 2 | More efficient handling of data 3 | General bug fixes and improvements 4 | Temporary move to the beta backend -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://alovoa.com/donate-list', 'https://www.buymeacoffee.com/alovoa'] 4 | ko_fi: Alovoa 5 | liberapay: alovoa/donate 6 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import React from 'react'; 3 | import { SvgProps } from "react-native-svg"; 4 | const content: React.FC; 5 | export default content; 6 | } -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/33.txt: -------------------------------------------------------------------------------- 1 | You can now show a preview of your own profile by tapping on your profile picture 2 | Randomize order of interests, images and prompts 3 | General UI improvements and bug fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | You can now see common interests in Search 2 | You can now see active state in Search and Profile 3 | Added warning for F-droid users 4 | Tons of bug fixes and visual improvements -------------------------------------------------------------------------------- /serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [{ 3 | "source": "**/*", 4 | "headers": [ 5 | { 6 | "key": "X-Frame-Options", 7 | "value": "deny" 8 | } 9 | ] 10 | }] 11 | } -------------------------------------------------------------------------------- /scripts/xml/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #121212 3 | #ec407a 4 | #023c69 5 | #ec407a 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/36.txt: -------------------------------------------------------------------------------- 1 | Change to and updated Search API 2 | Additional search setting fields 3 | Tons of new fields you can set in your profile 4 | Automatically hide suspicious profiles 5 | New advanced settings 6 | Bug fixes and improvements 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['@babel/plugin-transform-export-namespace-from', 6 | 'react-native-paper/babel', 7 | 'react-native-reanimated/plugin'], 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Ionicons } from "@expo/vector-icons"; 3 | import { IconT } from "../myTypes"; 4 | 5 | const Icon = ({ color, name, size, style }: IconT) => ( 6 | 7 | ); 8 | 9 | export default Icon; 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CardItemSearch } from "./CardItemSearch"; 2 | export { default as CardItemDonate } from "./CardItemDonate"; 3 | export { default as CardItemLikes } from "./CardItemLikes"; 4 | export { default as Filters } from "./Filters"; 5 | export { default as Icon } from "./Icon"; 6 | export { default as Message } from "./Message"; 7 | -------------------------------------------------------------------------------- /scripts/android.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | #create android project with some adjustments 5 | 6 | # create android and ios directories 7 | yarn expo prebuild --platform android --non-interactive --clean 8 | 9 | # dark splash - does not work with `eas build` 10 | cp ./scripts/xml/colors.xml \ 11 | ./android/app/src/main/res/values-night/colors.xml 12 | -------------------------------------------------------------------------------- /screens/profile/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Pictures } from "./Pictures"; 2 | export { default as ProfileSettings } from "./ProfileSettings"; 3 | export { default as SearchSettings } from "./SearchSettings"; 4 | export { default as Settings } from "./Settings"; 5 | export { default as AdvancedSettings } from "./AdvancedSettings"; 6 | export { default as Prompts } from "./Prompts"; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "jsx": "react-native", 6 | "lib": [ 7 | "dom", 8 | "esnext" 9 | ], 10 | "moduleResolution": "bundler", 11 | "noEmit": true, 12 | "skipLibCheck": true, 13 | "resolveJsonModule": true, 14 | "strict": true, 15 | }, 16 | "extends": "expo/tsconfig.base", 17 | } -------------------------------------------------------------------------------- /scripts/staging-web/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | cd ../.. 5 | git pull 6 | yarn 7 | oldpath=$(readlink dist) 8 | newpath="dist-$(uuidgen)" 9 | echo "$newpath" 10 | yarn expo export -p web --output-dir=$newpath 11 | ln -sfn $newpath dist 12 | 13 | read -p "Delete old directory? ($oldpath) " -n 1 -r 14 | echo # (optional) move to a new line 15 | if [[ $REPLY =~ ^[Yy]$ ]] 16 | then 17 | rm -rf $oldpath 18 | fi 19 | 20 | -------------------------------------------------------------------------------- /scripts/production-web/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | cd ../.. 5 | git pull 6 | yarn 7 | oldpath=$(readlink dist) 8 | newpath="dist-$(uuidgen)" 9 | echo "$newpath" 10 | yarn expo export -p web --output-dir=$newpath 11 | ln -sfn $newpath dist 12 | 13 | read -p "Delete old directory? ($oldpath) " -n 1 -r 14 | echo # (optional) move to a new line 15 | if [[ $REPLY =~ ^[Yy]$ ]] 16 | then 17 | rm -rf $oldpath 18 | fi 19 | 20 | -------------------------------------------------------------------------------- /components/Filters.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, TouchableOpacity } from "react-native"; 3 | import Icon from "./Icon"; 4 | import styles, { DARK_GRAY } from "../assets/styles"; 5 | 6 | const Filters = () => ( 7 | 8 | 9 | Filters 10 | 11 | 12 | ); 13 | 14 | export default Filters; 15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Native Android application for Alovoa, the open-source dating platform. 2 | 3 | Alovoa aims to be the first global open-source platform that enables people to easily find new friends and dates. 4 | 5 | Our promises: 6 | • No microtransaction 7 | • No annoying ads 8 | • No selling your data 9 | • Strict filters, only see people you want to see 10 | • Completely free (as in free speech) 11 | • Completely free (as in free beer) -------------------------------------------------------------------------------- /scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # a script to make updating package.json easier and more consistent 5 | 6 | # check what packages you want to upgrade 7 | yarn upgrade-interactive --latest --exact 8 | 9 | # downgrade any that are known to be too high 10 | yarn expo install --fix 11 | 12 | # use exact version (remove ~ & ^) 13 | perl -pi -e 's/(")[~^]/$1/g' package.json 14 | 15 | # make sure yarn.lock is up to date 16 | yarn install 17 | 18 | # check against the latest expo-doctor 19 | yarn doctor 20 | -------------------------------------------------------------------------------- /patches/README.md: -------------------------------------------------------------------------------- 1 | # node_modules patches 2 | 3 | [patch-package](https://github.com/ds300/patch-package) is used to fix issues related to various node_modules. these are applied automatically after `yarn install` via `postinstall` 4 | 5 | ```bash 6 | # apply patches to node_modules 7 | yarn patch-package 8 | ``` 9 | 10 | ```bash 11 | # un-apply patches to node_modules 12 | yarn patch-package --reverse 13 | ``` 14 | 15 | ```bash 16 | # create a new patch from updated node_modules 17 | yarn patch-package react-native-card-stack-swiper 18 | ``` 19 | -------------------------------------------------------------------------------- /i18n.tsx: -------------------------------------------------------------------------------- 1 | import * as Localization from 'expo-localization'; 2 | import { I18n } from 'i18n-js'; 3 | import translations from './i18n/' 4 | 5 | export function getI18n() : I18n { 6 | const i18n = new I18n(translations); 7 | const [locale] = Localization.getLocales(); 8 | i18n.locale = locale.languageTag; 9 | i18n.enableFallback = true; 10 | i18n.defaultLocale = 'en'; 11 | return i18n; 12 | } 13 | 14 | export function getLanguageTag(): string { 15 | let languageTag = Localization.getLocales().at(0)?.languageTag; 16 | return languageTag ? languageTag : "ja-JP"; 17 | } -------------------------------------------------------------------------------- /screens/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Search } from "./Search"; 2 | export { default as Likes } from "./Likes"; 3 | export { default as Messages } from "./Messages"; 4 | export { default as MessageDetail } from "./MessageDetail"; 5 | export { default as Profile } from "./Profile"; 6 | export { default as Donate } from "./Donate"; 7 | export { default as Register } from "./Register"; 8 | export { default as Onboarding } from "./Onboarding"; 9 | export { default as YourProfile } from "./YourProfile"; 10 | export { default as Login } from "./Login"; 11 | export { default as PasswordReset } from "./PasswordReset"; -------------------------------------------------------------------------------- /scripts/f-droid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | #create android project with some adjustments 5 | 6 | # small android adjustments 7 | ./scripts/android.sh 8 | 9 | #use older expo-location that can be easily patched 10 | yarn add expo-location@18.0.7 11 | 12 | #remove expo-dev-client since it has non-free deps and only needed in development 13 | #yarn remove expo-dev-client 14 | 15 | # apply node_module patches (`rm -rf node_modules && yarn` to reverse) 16 | yarn patch-package --patch-dir scripts/patches 17 | 18 | # remove signing 19 | perl -i -pe 's/^\s*signingConfig\s+.*\n//g' android/app/build.gradle 20 | -------------------------------------------------------------------------------- /plugins/setClearTextTrafficFalse.js: -------------------------------------------------------------------------------- 1 | const { createRunOncePlugin, withAndroidManifest } = require('@expo/config-plugins'); 2 | 3 | const setClearTextTrafficFalse = config => { 4 | return withAndroidManifest(config, config => { 5 | const androidManifest = config.modResults.manifest; 6 | const mainApplication = androidManifest.application[0]; 7 | mainApplication.$['android:usesCleartextTraffic'] = 'false'; 8 | return config; 9 | }); 10 | }; 11 | 12 | 13 | module.exports = createRunOncePlugin( 14 | setClearTextTrafficFalse, 15 | 'setClearTextTrafficFalse', 16 | '1.0.0' 17 | ); -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | 3 | module.exports = (() => { 4 | const config = getDefaultConfig(__dirname); 5 | 6 | const { transformer, resolver } = config; 7 | 8 | config.transformer = { 9 | ...transformer, 10 | babelTransformerPath: require.resolve("react-native-svg-transformer"), 11 | }; 12 | config.resolver = { 13 | ...resolver, 14 | assetExts: resolver.assetExts.filter((ext) => ext !== "svg"), 15 | sourceExts: [ 16 | ...new Set([ 17 | ...resolver.sourceExts, 18 | "svg", 19 | "ts", 20 | "tsx", 21 | ]), 22 | ], 23 | }; 24 | 25 | return config; 26 | })(); -------------------------------------------------------------------------------- /components/TabBarIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, View } from "react-native"; 3 | import Icon from "./Icon"; 4 | import styles from "../assets/styles"; 5 | import { TabBarIconT } from "../myTypes"; 6 | import { useTheme } from "react-native-paper"; 7 | 8 | const TabBarIcon = ({ focused, iconName, text }: TabBarIconT) => { 9 | const { colors } = useTheme(); 10 | const iconFocused = focused ? colors.primary : '#9e9e9e'; 11 | 12 | return ( 13 | 14 | 15 | {text} 16 | 17 | ); 18 | }; 19 | 20 | export default TabBarIcon; 21 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 15.0.3", 4 | "appVersionSource": "local" 5 | }, 6 | "build": { 7 | "development": { 8 | "developmentClient": true, 9 | "distribution": "internal", 10 | "channel": "development" 11 | }, 12 | "preview": { 13 | "distribution": "internal", 14 | "channel": "preview" 15 | }, 16 | "production": { 17 | "autoIncrement": false, 18 | "channel": "production" 19 | }, 20 | "development-simulator": { 21 | "developmentClient": true, 22 | "distribution": "internal", 23 | "ios": { 24 | "simulator": true 25 | }, 26 | "environment": "development", 27 | "channel": "development-simulator" 28 | } 29 | }, 30 | "submit": { 31 | "production": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugins/withGradleProperties.js: -------------------------------------------------------------------------------- 1 | const { withGradleProperties } = require('@expo/config-plugins'); 2 | 3 | module.exports = function withCustomGradleProps(config) { 4 | return withGradleProperties(config, (config) => { 5 | config.modResults.push({ 6 | type: 'property', 7 | key: 'org.gradle.jvmargs', 8 | value: '-Xmx6g -XX:MaxMetaspaceSize=3g -Dfile.encoding=UTF-8', 9 | }); 10 | config.modResults.push({ 11 | type: 'property', 12 | key: 'org.gradle.daemon', 13 | value: 'true', 14 | }); 15 | config.modResults.push({ 16 | type: 'property', 17 | key: 'android.dependencyMetadataInSigning', 18 | value: 'false', 19 | }); 20 | return config; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /scripts/patches/README.md: -------------------------------------------------------------------------------- 1 | # f-droid patches 2 | 3 | [patch-package](https://github.com/ds300/patch-package) is used to remove proprietary code from [f-droid builds](https://f-droid.org/en/packages/com.alovoa.expo/) 4 | 5 | ```bash 6 | # apply patches to node_modules 7 | yarn patch-package --patch-dir scripts/patches 8 | ``` 9 | 10 | ```bash 11 | # create a new patch from updated node_modules 12 | yarn patch-package expo-location --patch-dir scripts/patches 13 | ``` 14 | 15 | ## patch-archive 16 | 17 | patches no longer in use are located in `patch-archive` for reference 18 | 19 | ## expo-application 20 | 21 | patched to remove `com.android.installreferrer` 22 | dependency has been removed but patch kept for reference 23 | 24 | ## expo-location 25 | 26 | patched to remove `com.google.android.gms` & `io.nlopez.smartlocation` 27 | using `android.location.LocationManager` instead 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Check if you're in the correct repository** 11 | 12 | This repository only covers feature requests for app.alovoa.com and iOS/Android apps only. Go to [alovoa](https://github.com/Alovoa/alovoa) for requesting feature requests on app.alovoa.com and the backend. 13 | 14 | 15 | **Is your feature request related to a problem? Please describe.** 16 | 17 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 18 | 19 | **Describe the solution you'd like** 20 | 21 | A clear and concise description of what you want to happen. 22 | 23 | **Describe alternatives you've considered** 24 | 25 | A clear and concise description of any alternative solutions or features you've considered. 26 | 27 | **Additional context** 28 | 29 | Add any other context or screenshots about the feature request here. 30 | -------------------------------------------------------------------------------- /LICENSE_old: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Steven Persia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /patches/react-native-swiper-flatlist+3.2.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-swiper-flatlist/src/components/SwiperFlatList/SwiperFlatList.tsx b/node_modules/react-native-swiper-flatlist/src/components/SwiperFlatList/SwiperFlatList.tsx 2 | index 9f6aaf08bda7469116d703e28f5cb46e46305e7b..17c7915027e568f6e5f65a996d1227f3c87091a0 100644 3 | --- a/node_modules/react-native-swiper-flatlist/src/components/SwiperFlatList/SwiperFlatList.tsx 4 | +++ b/node_modules/react-native-swiper-flatlist/src/components/SwiperFlatList/SwiperFlatList.tsx 5 | @@ -233,9 +233,9 @@ export const SwiperFlatList = React.forwardRef( 6 | }, 7 | ]); 8 | 9 | - const flatListProps: FlatListProps & { ref: React.RefObject> } = { 10 | + const flatListProps: FlatListProps & { ref: React.RefObject> | null } = { 11 | scrollEnabled, 12 | - ref: flatListElement, 13 | + ref: flatListElement as React.RefObject>, 14 | keyExtractor: (_item, _index) => { 15 | const item = _item as { key?: string; id?: string }; 16 | const key = item?.key ?? item?.id ?? _index.toString(); 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .directory 2 | node_modules/**/* 3 | .expo/* 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | *.env* 12 | web-build/ 13 | android/* 14 | ios/* 15 | /dist* 16 | ssl-key 17 | 18 | # macOS 19 | .DS_Store 20 | 21 | # @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8 22 | # The following patterns were generated by expo-cli 23 | 24 | # OSX 25 | # 26 | .DS_Store 27 | 28 | # Xcode 29 | # 30 | build/ 31 | *.pbxuser 32 | !default.pbxuser 33 | *.mode1v3 34 | !default.mode1v3 35 | *.mode2v3 36 | !default.mode2v3 37 | *.perspectivev3 38 | !default.perspectivev3 39 | xcuserdata 40 | *.xccheckout 41 | *.moved-aside 42 | DerivedData 43 | *.aab 44 | *.apk 45 | *.hmap 46 | *.ipa 47 | *.xcuserstate 48 | project.xcworkspace 49 | 50 | # Android/IntelliJ 51 | # 52 | build/ 53 | .idea 54 | .gradle 55 | local.properties 56 | *.iml 57 | *.hprof 58 | 59 | # node.js 60 | # 61 | ./node_modules/ 62 | npm-debug.log 63 | yarn-error.log 64 | 65 | # BUCK 66 | buck-out/ 67 | \.buckd/ 68 | *.keystore 69 | !debug.keystore 70 | 71 | # Bundle artifacts 72 | *.jsbundle 73 | 74 | # CocoaPods 75 | /ios/Pods/ 76 | 77 | # Expo 78 | .expo/ 79 | credentials.json 80 | credentials/ 81 | web-build/ 82 | dist/ 83 | 84 | # @end expo-cli 85 | .vscode/settings.json 86 | -------------------------------------------------------------------------------- /assets/images/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Katerina Limpitsouni 2 | All images, assets and vectors published on unDraw can be used for free. You can use them for noncommercial and commercial purposes. You do not need to ask permission from or provide credit to the creator or unDraw. 3 | 4 | More precisely, unDraw grants you an nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use the assets provided from unDraw for free, including for commercial purposes, without permission from or attributing the creator or unDraw. This license does not include the right to compile assets, vectors or images from unDraw to replicate a similar or competing service, in any form or distribute the assets in packs or otherwise. This extends to automated and non-automated ways to link, embed, scrape, search or download the assets included on the website without our consent. 5 | 6 | Regarding brand logos that are included: 7 | Are registered trademarks of their respected owners. Are included on a promotional basis and do not represent an association with unDraw or its users. Do not indicate any kind of endorsement of the trademark holder towards unDraw, nor vice versa. Are provided with the sole purpose to represent the actual brand/service/company that has registered the trademark and must not be used otherwise. -------------------------------------------------------------------------------- /assets/onboarding/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Katerina Limpitsouni 2 | All images, assets and vectors published on unDraw can be used for free. You can use them for noncommercial and commercial purposes. You do not need to ask permission from or provide credit to the creator or unDraw. 3 | 4 | More precisely, unDraw grants you an nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use the assets provided from unDraw for free, including for commercial purposes, without permission from or attributing the creator or unDraw. This license does not include the right to compile assets, vectors or images from unDraw to replicate a similar or competing service, in any form or distribute the assets in packs or otherwise. This extends to automated and non-automated ways to link, embed, scrape, search or download the assets included on the website without our consent. 5 | 6 | Regarding brand logos that are included: 7 | Are registered trademarks of their respected owners. Are included on a promotional basis and do not represent an association with unDraw or its users. Do not indicate any kind of endorsement of the trademark holder towards unDraw, nor vice versa. Are provided with the sole purpose to represent the actual brand/service/company that has registered the trademark and must not be used otherwise. -------------------------------------------------------------------------------- /components/VerticalView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RefreshControl, RefreshControlProps, ScrollView, ScrollViewProps } from 'react-native'; 3 | import { WIDESCREEN_HORIZONTAL_MAX } from '../assets/styles'; 4 | import { useTheme } from 'react-native-paper'; 5 | import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; 6 | 7 | type Props = Pick | Pick 8 | const VerticalView = React.forwardRef(({ children, style, onRefresh = undefined }: any, ref) => { 9 | const { colors } = useTheme(); 10 | const [refreshing] = React.useState(false); // todo: setRefreshing 11 | return ( 12 | }>{children} 16 | ); 17 | }); 18 | VerticalView.displayName = 'VerticalView'; 19 | 20 | export default VerticalView; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Check if you're in the correct repository** 11 | 12 | This repository only covers bugs of app.alovoa.com and iOS/Android apps only. Go to [alovoa](https://github.com/Alovoa/alovoa) for reporting bugs on app.alovoa.com and the backend. 13 | 14 | **Describe the bug** 15 | 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | ** Device (please complete the following information):** 35 | - OS: [e.g. iOS, Windows, Ubuntu, Android] 36 | - OS version 37 | - Device: [e.g. iPhone6] 38 | 39 | **Native Application:** 40 | 41 | - Version 42 | - Origin [e.g. F-droid, Google Play Store, Apple Appstore] 43 | 44 | **Web application** 45 | 46 | - Browser [e.g. Firefox, Safari, Chrome] 47 | - Browser version 48 | 49 | **Additional context** 50 | 51 | Add any other context about the problem here. 52 | -------------------------------------------------------------------------------- /components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Dialog, Portal, Text } from 'react-native-paper'; 3 | import { AlertModel } from '../myTypes'; 4 | import { useWindowDimensions } from 'react-native'; 5 | import { WIDESCREEN_HORIZONTAL_MAX } from '../assets/styles'; 6 | 7 | const Alert = ({ visible = false, setVisible, message = "", buttons = [] }: AlertModel) => { 8 | const { width } = useWindowDimensions(); 9 | function calcMarginModal() { 10 | return width < WIDESCREEN_HORIZONTAL_MAX + 24 ? 24 : width / 5 + 24; 11 | } 12 | return ( 13 | 14 | setVisible(false)} style={{ marginHorizontal: calcMarginModal() }}> 15 | 16 | {message} 17 | 18 | 19 | { 20 | buttons.map((item, index) => ( 21 | 24 | )) 25 | } 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Alert; -------------------------------------------------------------------------------- /patches/patch-package+8.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/patch-package/dist/makePatch.js b/node_modules/patch-package/dist/makePatch.js 2 | index d8d09257767001ef7e30bba224d314ec2b2920b3..b04aa9d08b633fb95ca368a4aeb76345cd58aefc 100644 3 | --- a/node_modules/patch-package/dist/makePatch.js 4 | +++ b/node_modules/patch-package/dist/makePatch.js 5 | @@ -203,7 +203,17 @@ function makePatch({ packagePathSpecifier, appPath, packageManager, includePaths 6 | // stage all files 7 | git("add", "-f", packageDetails.path); 8 | // get diff of changes 9 | - const diffResult = git("diff", "--cached", "--no-color", "--ignore-space-at-eol", "--no-ext-diff", "--src-prefix=a/", "--dst-prefix=b/"); 10 | + const diffResult = git( 11 | + "diff", 12 | + "--cached", 13 | + "--no-color", 14 | + "--ignore-space-at-eol", 15 | + "--no-ext-diff", 16 | + "--src-prefix=a/", 17 | + "--dst-prefix=b/", 18 | + "--patience", // use the patience diff algorithm to increase readability of patches 19 | + "--full-index", // use full-index to better support blobs such as io.nlopez.smartlocation-3.3.3-jetified.aar 20 | + ); 21 | if (diffResult.stdout.length === 0) { 22 | console_1.default.log(`⁉️ Not creating patch file for package '${packagePathSpecifier}'`); 23 | console_1.default.log(`⁉️ There don't appear to be any changes.`); 24 | -------------------------------------------------------------------------------- /components/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Image, TouchableOpacity, useWindowDimensions } from "react-native"; 3 | import { Text } from "react-native-paper"; 4 | import { MessageT } from "../myTypes"; 5 | import styles, { WIDESCREEN_HORIZONTAL_MAX } from "../assets/styles"; 6 | import * as I18N from "../i18n"; 7 | import * as Global from "../Global"; 8 | 9 | const i18n = I18N.getI18n() 10 | 11 | const Message = ({ conversation }: MessageT) => { 12 | 13 | let text: string = !conversation.lastMessage ? "" : conversation.lastMessage.from ? "" : i18n.t('you') + ": "; 14 | text += conversation.lastMessage ? conversation.lastMessage.content : i18n.t('chat.default'); 15 | const { width } = useWindowDimensions(); 16 | 17 | return ( 18 | 19 | 20 | Global.nagivateProfile(undefined, conversation.uuid)} > 21 | 22 | 23 | 24 | 25 | Global.nagivateChatDetails(conversation)} > 26 | {conversation.userName} 27 | WIDESCREEN_HORIZONTAL_MAX ? WIDESCREEN_HORIZONTAL_MAX : width) - 120}]}>{text} 28 | 29 | 30 | 31 | ) 32 | }; 33 | 34 | export default Message; 35 | -------------------------------------------------------------------------------- /components/CardItemDonate.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Image, TouchableOpacity, StyleProp, TextStyle, useWindowDimensions } from "react-native"; 3 | import { useTheme, Text } from "react-native-paper"; 4 | import { CardItemT } from "../myTypes"; 5 | import * as Global from "../Global"; 6 | import styles, { 7 | WIDESCREEN_HORIZONTAL_MAX 8 | } from "../assets/styles"; 9 | 10 | const CardItem = ({ 11 | user, 12 | donation 13 | }: CardItemT) => { 14 | 15 | const { colors } = useTheme(); 16 | const { width } = useWindowDimensions(); 17 | 18 | // Custom styling 19 | const cardPadding = 30; 20 | 21 | const imageStyle = [ 22 | { 23 | borderRadius: 8, 24 | width: width / 2 - cardPadding, 25 | height: width / 2 - cardPadding, 26 | maxWidth: WIDESCREEN_HORIZONTAL_MAX / 2 - cardPadding, 27 | maxHeight: WIDESCREEN_HORIZONTAL_MAX / 2 - cardPadding, 28 | }, 29 | ]; 30 | 31 | const nameStyle: StyleProp = [ 32 | { 33 | paddingTop: 10, 34 | paddingBottom: 5, 35 | fontSize: 15, 36 | textAlign: 'center', 37 | textAlignVertical: 'center' 38 | }, 39 | ]; 40 | 41 | return ( 42 | 43 | {/* IMAGE */} 44 | Global.nagivateProfile(user)}> 45 | 46 | 47 | 48 | {/* NAME */} 49 | 50 | {user.firstName + ", " + user.age} 51 | 52 | 53 | {donation + ' €'} 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default CardItem; 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build Android APK (Yarn, No EAS) 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | build-apk: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout source 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js (Expo 54 requires Node 20+) 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: yarn 21 | 22 | - name: Setup Java (JDK 17) 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: temurin 26 | java-version: 17 27 | 28 | - name: Verify Java version 29 | run: java -version 30 | 31 | - name: Install Android SDK 32 | uses: android-actions/setup-android@v3 33 | 34 | - name: Install dependencies (Yarn) 35 | run: yarn install --frozen-lockfile 36 | 37 | - name: Run Yarn Doctor (dependency verification) 38 | run: yarn doctor 39 | 40 | - name: Build with F-droid build process 41 | run: yarn f-droid 42 | 43 | - name: Give Gradle permission 44 | run: chmod +x android/gradlew 45 | 46 | - name: Configure Android SDK path 47 | run: echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties 48 | 49 | - name: Configure Gradle memory (CI safe) 50 | run: | 51 | mkdir -p ~/.gradle 52 | cat < ~/.gradle/gradle.properties 53 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 54 | kotlin.daemon.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1g 55 | org.gradle.parallel=false 56 | org.gradle.daemon=false 57 | org.gradle.caching=false 58 | ksp.useKSP2=false 59 | ksp.incremental=false 60 | EOF 61 | 62 | - name: Build APK (Release) 63 | run: | 64 | cd android 65 | ./gradlew assembleRelease --no-daemon 66 | 67 | - name: Upload APK 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: app-release 71 | path: android/app/build/outputs/apk/release/app-release.apk 72 | 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | **Alovoa - Expo** 4 | 5 | React Native mobile application for Alovoa. 6 | 7 |

8 | 9 |

10 | 11 |

12 | Get it on F-Droid 13 | Get it on Google Play 14 |

15 | 16 | ### Contribute 17 | 18 | - Tell your friends about it and share on social media! This is the best way to make it grow. 19 | - Improve the project by posting in [Issues](https://github.com/Alovoa/alovoa-expo/issues) and make a PR upon Issue discussion. 20 | - Translate this project into your preferred language on [Weblate](https://hosted.weblate.org/projects/alovoa-expo/alovoa-expo/) 21 | 22 | ## Installation and usage 23 | 24 | Be sure, you have installed all dependencies and applications to run Expo project on your computer : [Getting Started with Expo](https://docs.expo.io/get-started/installation/). 25 | 26 | ### Running the project 27 | 28 | Clone this repository : 29 | 30 | ```bash 31 | git clone https://github.com/Alovoa/alovoa-expo 32 | cd alovoa-expo 33 | ``` 34 | 35 | Install packages : 36 | 37 | ```bash 38 | yarn 39 | ``` 40 | 41 | When installation is complete, run it : 42 | 43 | ```bash 44 | yarn start 45 | ``` 46 | 47 | To create a native Android project: 48 | 49 | ```bash 50 | # creates ./android 51 | ./scripts/android.sh 52 | ``` 53 | 54 | This project is based by this [amazing stevenpersia's repository](https://github.com/stevenpersia/tinder-expo). Feel free to follow this guy because he does great stuff. Any code before my [initial commit](https://github.com/Alovoa/alovoa-expo/commit/5b4acdfbd1f54b46d65ffffb1c8e98fb0eff246a) is [MIT licensed](https://github.com/Alovoa/alovoa-expo/blob/master/LICENSE_old). The main goal of this project is to create native mobile apps for Android and iOS for Alovoa. 55 | -------------------------------------------------------------------------------- /patches/react-native-card-stack-swiper+1.2.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-card-stack-swiper/Card.js b/node_modules/react-native-card-stack-swiper/Card.js 2 | index 4d89c12..63094a9 100644 3 | --- a/node_modules/react-native-card-stack-swiper/Card.js 4 | +++ b/node_modules/react-native-card-stack-swiper/Card.js 5 | @@ -4,7 +4,16 @@ import { 6 | View, 7 | } from 'react-native'; 8 | 9 | -const Card = ({ style, children }) => ( 10 | +// Warning: Card: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead. 11 | +const Card = ({ 12 | + style = {}, 13 | + children, 14 | + onSwiped = () => {}, 15 | + onSwipedLeft = () => {}, 16 | + onSwipedRight = () => {}, 17 | + onSwipedTop = () => {}, 18 | + onSwipedBottom = () => {}, 19 | +}) => ( 20 | 21 | {children} 22 | ); 23 | @@ -18,13 +27,5 @@ Card.propTypes = { 24 | onSwipedBottom: PropTypes.func, 25 | onSwiped: PropTypes.func, 26 | } 27 | -Card.defaultProps = { 28 | - style:{}, 29 | - onSwiped: () => {}, 30 | - onSwipedLeft: () => {}, 31 | - onSwipedRight: () => {}, 32 | - onSwipedTop: () => {}, 33 | - onSwipedBottom: () => {}, 34 | -} 35 | 36 | export default Card; 37 | diff --git a/node_modules/react-native-card-stack-swiper/index.d.ts b/node_modules/react-native-card-stack-swiper/index.d.ts 38 | index 112c474..862a215 100644 39 | --- a/node_modules/react-native-card-stack-swiper/index.d.ts 40 | +++ b/node_modules/react-native-card-stack-swiper/index.d.ts 41 | @@ -2,6 +2,7 @@ import * as React from 'react'; 42 | import { StyleProp, ViewProps, ViewStyle } from 'react-native'; 43 | 44 | export interface CardStackProps { 45 | + children?: React.ReactNode; // TS2322: Type '{ children: Element[]; ... }' is not assignable to type ... 46 | style?: StyleProp; 47 | secondCardZoom?: number; 48 | loop?: boolean; 49 | @@ -38,6 +39,7 @@ export default class CardStack extends React.Component { 50 | } 51 | 52 | export interface CardProps { 53 | + children?: React.ReactNode; // TS2322: Type '{ children: Element; ... }' is not assignable to type ... 54 | style?: StyleProp; 55 | onSwiped?: () => void; 56 | onSwipedLeft?: () => void; 57 | -------------------------------------------------------------------------------- /screens/Messages.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, useWindowDimensions } from "react-native"; 3 | import { Text } from "react-native-paper"; 4 | import { Message } from "../components"; 5 | import { ChatsResource, ConversationDto, RootStackParamList } from "../myTypes"; 6 | import { STATUS_BAR_HEIGHT } from "../assets/styles"; 7 | import ConvoEmpty from "../assets/images/convo-empty.svg"; 8 | import * as Global from "../Global"; 9 | import * as URL from "../URL"; 10 | import * as I18N from "../i18n"; 11 | import VerticalView from "../components/VerticalView"; 12 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 13 | 14 | type Props = BottomTabScreenProps 15 | 16 | const Messages = ({ navigation }: Props) => { 17 | 18 | const i18n = I18N.getI18n() 19 | 20 | const [loaded, setLoaded] = React.useState(false); 21 | const [results, setResults] = React.useState(Array); 22 | const { height } = useWindowDimensions(); 23 | 24 | const svgHeight = 150; 25 | const svgWidth = 200; 26 | 27 | async function load() { 28 | let response = await Global.Fetch(URL.API_RESOURCE_CHATS); 29 | let data: ChatsResource = response.data; 30 | setResults(data.conversations); 31 | setLoaded(true); 32 | } 33 | 34 | React.useEffect(() => { 35 | const unsubscribe = navigation.addListener('focus', () => { 36 | load(); 37 | }); 38 | return unsubscribe; 39 | }, [navigation]); 40 | 41 | return ( 42 | 43 | 44 | { 45 | results.map((item, index) => ( 46 | 49 | )) 50 | } 51 | {results && results.length === 0 && loaded && 52 | 53 | 54 | {i18n.t('convo-empty.title')} 55 | {i18n.t('convo-empty.subtitle')} 56 | 57 | } 58 | 59 | ) 60 | }; 61 | 62 | export default Messages; 63 | -------------------------------------------------------------------------------- /components/CardItemLikes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Image, TouchableOpacity, StyleProp, TextStyle, useWindowDimensions } from "react-native"; 3 | import { useTheme, Text, IconButton } from "react-native-paper"; 4 | import { CardItemT, LikeResultT } from "../myTypes"; 5 | import * as Global from "../Global"; 6 | import styles, { 7 | WIDESCREEN_HORIZONTAL_MAX 8 | } from "../assets/styles"; 9 | 10 | const CardItem = ({ 11 | user, message, onMessagePressed 12 | }: CardItemT) => { 13 | 14 | const { colors } = useTheme(); 15 | const { width } = useWindowDimensions(); 16 | const iconSize = 28; 17 | 18 | // Custom styling 19 | const cardPadding = 30; 20 | 21 | const imageStyle = [ 22 | { 23 | borderRadius: 8, 24 | width: width / 2 - cardPadding, 25 | height: width / 2 - cardPadding, 26 | maxWidth: WIDESCREEN_HORIZONTAL_MAX / 2 - cardPadding, 27 | maxHeight: WIDESCREEN_HORIZONTAL_MAX / 2 - cardPadding, 28 | }, 29 | ]; 30 | 31 | const nameStyle: StyleProp = [ 32 | { 33 | paddingTop: 10, 34 | paddingBottom: 5, 35 | fontSize: 15, 36 | textAlign: 'center', 37 | textAlignVertical: 'center' 38 | }, 39 | ]; 40 | 41 | return ( 42 | 43 | {/* IMAGE */} 44 | Global.nagivateProfile(user)}> 45 | 46 | 47 | 48 | {/* NAME */} 49 | 50 | {user.firstName + ", " + user.age} 51 | 52 | 53 | 54 | {message && 55 | 56 | { 61 | if (onMessagePressed) { 62 | let t = {} as LikeResultT; 63 | t.message = message; 64 | t.user = user; 65 | onMessagePressed(t); 66 | } 67 | }} 68 | /> 69 | 70 | } 71 | 72 | ); 73 | }; 74 | 75 | export default CardItem; 76 | -------------------------------------------------------------------------------- /components/InterestModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { InterestModalT, UserInterest, } from "../myTypes"; 3 | import { Text, Button, useTheme, IconButton, Badge } from 'react-native-paper'; 4 | import { KeyboardAvoidingView, View, useWindowDimensions } from "react-native"; 5 | import * as Global from "../Global"; 6 | import * as I18N from "../i18n"; 7 | import styles, { WIDESCREEN_HORIZONTAL_MAX } from "../assets/styles"; 8 | import InterestView from "./InterestView"; 9 | import Modal from "react-native-modal"; 10 | 11 | const InterestModal = ({ user }: InterestModalT) => { 12 | 13 | const i18n = I18N.getI18n(); 14 | const { colors } = useTheme(); 15 | const { width } = useWindowDimensions(); 16 | 17 | const [buttonText, setButtonText] = React.useState(""); 18 | const [visible, setVisible] = React.useState(false); 19 | const [interests, setInterests] = React.useState(user?.interests); 20 | 21 | const showModal = () => setVisible(true); 22 | const hideModal = () => setVisible(false); 23 | const containerStyle = { backgroundColor: colors.background, padding: 24, marginHorizontal: calcMarginModal(), borderRadius: 8 }; 24 | 25 | function calcMarginModal() { 26 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 27 | } 28 | 29 | function updateButtonText(interests: UserInterest[]) { 30 | let text = interests.map(item => item.text).join(", "); 31 | if (!text) { 32 | text = Global.EMPTY_STRING; 33 | } 34 | setButtonText(text); 35 | } 36 | 37 | React.useEffect(() => { 38 | if (user) updateButtonText(user.interests); 39 | }, []); 40 | 41 | return ( 42 | 43 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | {i18n.t('profile.onboarding.interests')} 63 | 64 | 66 | 67 | ); 68 | }; 69 | 70 | export default InterestModal; 71 | -------------------------------------------------------------------------------- /components/ComplimentModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IconButton, Text, TextInput, useTheme } from 'react-native-paper'; 3 | import { ComplimentModalT } from '../myTypes'; 4 | import { View, Image, useWindowDimensions, KeyboardAvoidingView } from 'react-native'; 5 | import { WIDESCREEN_HORIZONTAL_MAX } from '../assets/styles'; 6 | import * as I18N from "../i18n"; 7 | import Modal from 'react-native-modal'; 8 | 9 | const ComplimentModal = ({ 10 | visible = false, 11 | setVisible, 12 | name, 13 | age, 14 | profilePicture, 15 | onSend, 16 | onDismiss 17 | }: ComplimentModalT) => { 18 | 19 | const { width } = useWindowDimensions(); 20 | const i18n = I18N.getI18n(); 21 | const { colors } = useTheme(); 22 | const [text, setText] = React.useState(""); 23 | 24 | const maxLength = 120; 25 | 26 | const containerStyle = { 27 | backgroundColor: colors.elevation.level2, 28 | padding: 24, 29 | marginHorizontal: calcMarginModal(), 30 | borderRadius: 8 31 | }; 32 | 33 | function calcMarginModal() { 34 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 35 | } 36 | 37 | const hideModal = () => { 38 | setVisible(false); 39 | if (onDismiss) onDismiss(); 40 | }; 41 | 42 | // Reset field when new profile opens 43 | React.useEffect(() => { 44 | setText(""); 45 | }, [profilePicture]); 46 | 47 | return ( 48 | ( 49 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 75 | {`${name}, ${age}`} 76 | 77 | 78 | onSend(text, true)} 84 | placeholder={i18n.t('compliment.title')} 85 | maxLength={maxLength} 86 | autoCorrect={false} 87 | right={ 88 | onSend(text, true)} 91 | icon="send" 92 | /> 93 | } 94 | /> 95 | 96 | 97 | 98 | )); 99 | }; 100 | 101 | export default ComplimentModal; 102 | -------------------------------------------------------------------------------- /app.config.js: -------------------------------------------------------------------------------- 1 | const EAS_PROJECT_ID = process.env.EAS_PROJECT_ID; 2 | const EXPO_OWNER = process.env.EXPO_OWNER; 3 | 4 | module.exports = { 5 | "expo": { 6 | "name": "Alovoa", 7 | "slug": "alovoa-expo", 8 | "version": "2.3.1", 9 | "scheme": "alovoa", 10 | "orientation": "portrait", 11 | "userInterfaceStyle": "automatic", 12 | "icon": "./assets/icon.png", 13 | "newArchEnabled": true, 14 | "plugins": [ 15 | "expo-font", 16 | "expo-secure-store", 17 | "expo-web-browser", 18 | [ 19 | "expo-image-picker", { 20 | "photosPermission": "The app accesses your photos to let you share them with other users." 21 | } 22 | ], 23 | "./plugins/setClearTextTrafficFalse", 24 | "./plugins/withGradleProperties", 25 | "expo-localization", 26 | "expo-build-properties" 27 | ], 28 | "splash": { 29 | "image": "./assets/splash.png", 30 | "resizeMode": "contain", 31 | "backgroundColor": "#ec407a", 32 | "dark": { 33 | "image": "./assets/splash.png", 34 | "backgroundColor": "#121212" 35 | } 36 | }, 37 | "updates": { 38 | "enabled": false, 39 | "checkAutomatically": "ON_ERROR_RECOVERY", 40 | "url": EAS_PROJECT_ID ? `https://u.expo.dev/${EAS_PROJECT_ID}` : "https://github.com/Alovoa/alovoa-expo/releases/latest" 41 | }, 42 | "assetBundlePatterns": [ 43 | "**/*" 44 | ], 45 | "ios": { 46 | "supportsTablet": true, 47 | "usesAppleSignIn": true, 48 | "bundleIdentifier": "com.alovoa.expo", 49 | "associatedDomains": [ 50 | "applinks:alovoa.com" 51 | ], 52 | "infoPlist": { 53 | "LSApplicationQueriesSchemes": [ 54 | "alovoa" 55 | ], 56 | "ITSAppUsesNonExemptEncryption": false, 57 | "NSLocationWhenInUseUsageDescription": "This app uses the location to list other users in close proximity", 58 | "NSDocumentsFolderUsageDescription": "This app uses the Documents folder to store the requested user data", 59 | "NSMicrophoneUsageDescription": "This app uses the microphone to record the users voice for other users" 60 | }, 61 | "buildNumber": "37" 62 | }, 63 | "android": { 64 | "icon": "./assets/icon-round.png", 65 | "adaptiveIcon": { 66 | "foregroundImage": "./assets/adaptive-icon.png", 67 | "monochromeImage": "./assets/monochrome-icon.png", 68 | "backgroundColor": "#ec407a" 69 | }, 70 | "intentFilters": [ 71 | { 72 | "action": "VIEW", 73 | "data": [ 74 | { 75 | "scheme": "alovoa" 76 | } 77 | ], 78 | "category": [ 79 | "BROWSABLE", 80 | "DEFAULT" 81 | ] 82 | } 83 | ], 84 | "package": "com.alovoa.expo", 85 | "softwareKeyboardLayoutMode": "pan", 86 | "permissions": [ 87 | "android.permission.ACCESS_COARSE_LOCATION", 88 | "android.permission.ACCESS_FINE_LOCATION", 89 | "android.permission.RECORD_AUDIO" 90 | ], 91 | "lintOptions": { 92 | "checkReleaseBuilds": false, 93 | "abortOnError": false 94 | }, 95 | "versionCode": 45 96 | }, 97 | "web": { 98 | "favicon": "./assets/favicon.png" 99 | }, 100 | "extra": { 101 | "eas": { 102 | "projectId": EAS_PROJECT_ID 103 | } 104 | }, 105 | "owner": EXPO_OWNER, 106 | "runtimeVersion": { 107 | "policy": "appVersion" 108 | } 109 | }, 110 | "build": { 111 | "android": { 112 | "env": { 113 | "ORG_GRADLE_JVMARGS": "-Xmx6g -XX:MaxMetaspaceSize=3g -Dfile.encoding=UTF-8" 114 | } 115 | } 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /components/SelectModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SelectModalT } from "../myTypes"; 3 | import { Modal, Portal, Text, Button, Checkbox, useTheme, IconButton } from 'react-native-paper'; 4 | import { View, useWindowDimensions } from "react-native"; 5 | import * as I18N from "../i18n"; 6 | import { WIDESCREEN_HORIZONTAL_MAX } from "../assets/styles"; 7 | import * as Global from "../Global"; 8 | 9 | const SelectModal = ({ multi = false, disabled = false, minItems = 0, title, data, selected, onValueChanged }: SelectModalT) => { 10 | 11 | const i18n = I18N.getI18n(); 12 | const { colors } = useTheme(); 13 | const { width } = useWindowDimensions(); 14 | 15 | const [selectedIds, setSelectedIds] = React.useState(selected); 16 | const [buttonText, setButtonText] = React.useState(""); 17 | const [visible, setVisible] = React.useState(false); 18 | const [buttonDisabled, setButtonDisabled] = React.useState(disabled); 19 | const showModal = () => setVisible(true); 20 | const hideModal = () => setVisible(false); 21 | const containerStyle = { backgroundColor: colors.background, padding: 24, marginHorizontal: calcMarginModal(), borderRadius: 8 }; 22 | 23 | function calcMarginModal() { 24 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 25 | } 26 | 27 | function updateButtonText() { 28 | let text = [...data.entries()].filter(([key, value]) => 29 | selectedIds.includes(value[0])).map(([key, value]) => value[1] !== undefined ? i18n.t(value[1]) : '').join(", "); 30 | if(!text) { 31 | text = Global.EMPTY_STRING; 32 | } 33 | setButtonText(text); 34 | } 35 | 36 | React.useEffect(() => { 37 | updateButtonText(); 38 | }, [selectedIds, data]); 39 | 40 | React.useEffect(() => { 41 | updateButtonText(); 42 | setSelectedIds(selected); 43 | }, [selected]); 44 | 45 | React.useEffect(() => { 46 | setButtonDisabled(disabled); 47 | }, [disabled]); 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 60 | 61 | {title} 62 | 63 | {[...data].map(([key, value], index) => ( 64 | { 67 | let hasItem = selectedIds.includes(key); 68 | if (multi) { 69 | if (hasItem) { 70 | let copy = selectedIds.filter(s => s !== key); 71 | if (copy.length >= minItems) { 72 | setSelectedIds(copy); 73 | } 74 | } else { 75 | const copy = [...selectedIds]; 76 | copy.push(key) 77 | setSelectedIds(copy); 78 | } 79 | onValueChanged(key, !hasItem); 80 | } else { 81 | setSelectedIds([key]); 82 | onValueChanged(key, true); 83 | hideModal(); 84 | } 85 | }} 86 | /> 87 | ))} 88 | 89 | 90 | 91 | {title} 92 | 94 | 95 | ); 96 | }; 97 | 98 | export default SelectModal; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alovoa-expo", 3 | "version": "2.1.0", 4 | "private": true, 5 | "packageManager": "yarn@1.22.22", 6 | "scripts": { 7 | "android": "expo run:android", 8 | "doctor": "npx expo-doctor@latest", 9 | "eas": "npx eas-cli@latest", 10 | "f-droid": "./scripts/f-droid.sh", 11 | "ios": "expo run:ios", 12 | "lint": "eslint . --max-warnings 0", 13 | "postinstall": "patch-package", 14 | "prebuild": "expo prebuild", 15 | "start": "expo start", 16 | "type-check": "tsc --noEmit", 17 | "update": "./scripts/update.sh", 18 | "web": "expo start --web" 19 | }, 20 | "dependencies": { 21 | "@expo-google-fonts/montserrat": "0.2.3", 22 | "@expo/config": "~12.0.7", 23 | "@expo/metro-runtime": "~6.1.2", 24 | "@expo/vector-icons": "^15.0.3", 25 | "@likashefqet/react-native-image-zoom": "4.3.0", 26 | "@react-native-async-storage/async-storage": "2.2.0", 27 | "@react-native-community/slider": "5.0.1", 28 | "@react-navigation/bottom-tabs": "^7.8.6", 29 | "@react-navigation/elements": "2.2.6", 30 | "@react-navigation/native": "7.0.15", 31 | "@react-navigation/stack": "7.1.2", 32 | "axios": "^1.13.2", 33 | "expo": "54.0.27", 34 | "expo-apple-authentication": "~8.0.8", 35 | "expo-build-properties": "~1.0.10", 36 | "expo-clipboard": "~8.0.8", 37 | "expo-constants": "~18.0.11", 38 | "expo-device": "~8.0.10", 39 | "expo-file-system": "~19.0.20", 40 | "expo-font": "~14.0.10", 41 | "expo-image-manipulator": "~14.0.8", 42 | "expo-image-picker": "~17.0.9", 43 | "expo-linking": "~8.0.10", 44 | "expo-localization": "~17.0.8", 45 | "expo-location": "19.0.7", 46 | "expo-screen-orientation": "~9.0.8", 47 | "expo-secure-store": "~15.0.8", 48 | "expo-sharing": "~14.0.8", 49 | "expo-splash-screen": "~31.0.12", 50 | "expo-status-bar": "~3.0.9", 51 | "expo-system-ui": "~6.0.9", 52 | "expo-updates": "~29.0.15", 53 | "expo-web-browser": "~15.0.10", 54 | "form-data": "4.0.2", 55 | "i18n-js": "4.5.1", 56 | "lodash": "4.17.21", 57 | "mime": "4.0.6", 58 | "react": "19.1.0", 59 | "react-dom": "19.1.0", 60 | "react-native": "0.81.5", 61 | "react-native-autolink": "4.2.0", 62 | "react-native-card-stack-swiper": "1.2.5", 63 | "react-native-gesture-handler": "~2.28.0", 64 | "react-native-keyboard-aware-scroll-view": "^0.9.5", 65 | "react-native-modal": "^14.0.0-rc.1", 66 | "react-native-paper": "^5.14.5", 67 | "react-native-paper-dates": "0.22.34", 68 | "react-native-reanimated": "~4.1.1", 69 | "react-native-safe-area-context": "~5.6.0", 70 | "react-native-screens": "~4.16.0", 71 | "react-native-svg": "15.12.1", 72 | "react-native-swiper-flatlist": "3.2.5", 73 | "react-native-toast-message": "2.2.1", 74 | "react-native-vector-icons": "10.2.0", 75 | "react-native-web": "^0.21.0", 76 | "react-native-worklets": "0.5.1", 77 | "reanimated-color-picker": "3.0.6", 78 | "serve": "14.2.4" 79 | }, 80 | "devDependencies": { 81 | "@babel/core": "7.26.9", 82 | "@babel/plugin-proposal-export-default-from": "7.25.9", 83 | "@babel/plugin-syntax-export-default-from": "7.25.9", 84 | "@babel/plugin-transform-flow-strip-types": "7.26.5", 85 | "@babel/plugin-transform-runtime": "7.26.9", 86 | "@babel/preset-env": "7.26.9", 87 | "@types/lodash": "4.17.16", 88 | "@types/react": "19.1.10", 89 | "eslint": "8.57.1", 90 | "eslint-config-expo": "10.0.0", 91 | "expo-module-scripts": "^5.0.7", 92 | "metro-react-native-babel-transformer": "^0.77.0", 93 | "patch-package": "8.0.0", 94 | "postinstall-postinstall": "2.1.0", 95 | "react-native-svg-transformer": "1.5.0", 96 | "typescript": "5.9.2" 97 | }, 98 | "expo": { 99 | "install": { 100 | "exclude": [ 101 | "expo-location" 102 | ] 103 | }, 104 | "doctor": { 105 | "reactNativeDirectoryCheck": { 106 | "exclude": [ 107 | "form-data", 108 | "lodash", 109 | "mime", 110 | "serve", 111 | "react-native-autolink" 112 | ] 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /assets/data/demo.ts: -------------------------------------------------------------------------------- 1 | import { DataT } from "../../myTypes"; 2 | import icon from '../icon.png'; 3 | 4 | const IMAGE_01 = icon, 5 | IMAGE_02 = icon, 6 | IMAGE_03 = icon, 7 | IMAGE_04 = icon, 8 | IMAGE_05 = icon, 9 | IMAGE_06 = icon, 10 | IMAGE_07 = icon, 11 | IMAGE_08 = icon, 12 | IMAGE_09 = icon, 13 | IMAGE_10 = icon; 14 | 15 | const data: DataT[] = [ 16 | { 17 | id: 1, 18 | name: "Leanneana", 19 | isOnline: true, 20 | match: "78", 21 | description: 22 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut a.", 23 | message: 24 | "I will go back to Gotham and I will fight men Iike this but I will not become an executioner.", 25 | image: IMAGE_01, 26 | }, 27 | { 28 | id: 2, 29 | name: "Clementine Bauch", 30 | match: "93", 31 | description: 32 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 33 | isOnline: false, 34 | message: "Someone like you. Someone who'll rattle the cages.", 35 | image: IMAGE_02, 36 | }, 37 | { 38 | id: 3, 39 | name: "Ervin Howell", 40 | match: "45", 41 | description: 42 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 43 | isOnline: false, 44 | message: 45 | "Oh, hee-hee, aha. Ha, ooh, hee, ha-ha, ha-ha. And I thought my jokes were bad.", 46 | image: IMAGE_03, 47 | }, 48 | { 49 | id: 4, 50 | name: "John Lebsack", 51 | match: "88", 52 | description: 53 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 54 | isOnline: true, 55 | message: "Bats frighten me. It's time my enemies shared my dread.", 56 | image: IMAGE_04, 57 | }, 58 | { 59 | id: 5, 60 | name: "James Dietrich", 61 | match: "76", 62 | description: 63 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 64 | isOnline: false, 65 | message: "It's not who I am underneath but what I do that defines me.", 66 | image: IMAGE_05, 67 | }, 68 | { 69 | id: 6, 70 | name: "Patricia Schulist", 71 | match: "95", 72 | description: 73 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 74 | isOnline: true, 75 | message: 76 | "You have nothing, nothing to threaten me with. Nothing to do with all your strength.", 77 | image: IMAGE_06, 78 | }, 79 | { 80 | id: 7, 81 | name: "Chelsey Weissnat", 82 | match: "67", 83 | description: 84 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 85 | isOnline: true, 86 | message: 87 | "Never start with the head. The victim gets all fuzzy. He can't feel the next... See?", 88 | image: IMAGE_07, 89 | }, 90 | { 91 | id: 8, 92 | name: "Nicky Runol", 93 | match: "85", 94 | description: 95 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 96 | age: "27", 97 | location: "Irvine, CA", 98 | info1: 'Straight, Single, 5"10', 99 | info2: "Tea Totaller, Loves Photography & Travel", 100 | info3: "Beaches, Mountain, Cafe, Movies", 101 | info4: "Last seen: 23h ago", 102 | isOnline: true, 103 | message: 104 | "And as for the television's so-called plan, Batman has no jurisdiction.", 105 | image: IMAGE_08, 106 | }, 107 | { 108 | id: 9, 109 | name: "Glenna Reichert", 110 | match: "74", 111 | description: 112 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 113 | isOnline: true, 114 | message: 115 | "This is what happens when an unstoppable force meets an immovable object.", 116 | image: IMAGE_09, 117 | }, 118 | { 119 | id: 10, 120 | name: "Kurtis DuBuque", 121 | match: "98", 122 | description: 123 | "Full-time Traveller. Globe Trotter. Occasional Photographer. Part time Singer/Dancer.", 124 | isOnline: false, 125 | message: 126 | "You want order in Gotham. Batman must take off his mask and turn himself in.", 127 | image: IMAGE_10, 128 | }, 129 | ]; 130 | 131 | export default data; 132 | -------------------------------------------------------------------------------- /screens/profile/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | useWindowDimensions 5 | } from "react-native"; 6 | import styles from "../../assets/styles"; 7 | import { RootStackParamList, SettingsEmailEnum, SettingsEmailNameMap, UnitsEnum, UnitsNameMap, YourProfileResource } from "../../myTypes"; 8 | import * as I18N from "../../i18n"; 9 | import * as Global from "../../Global"; 10 | import * as URL from "../../URL"; 11 | import SelectModal from "../../components/SelectModal"; 12 | import VerticalView from "../../components/VerticalView"; 13 | import ColorModal from "../../components/ColorModal"; 14 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 15 | 16 | const i18n = I18N.getI18n(); 17 | 18 | type Props = BottomTabScreenProps 19 | 20 | const Settings = ({ route }: Props) => { 21 | 22 | const data: YourProfileResource = route.params.data; 23 | 24 | const { height } = useWindowDimensions(); 25 | const [units, setUnits] = React.useState(UnitsEnum.SI); 26 | const [emailSettings, setEmailSettings] = React.useState>(new Map()); 27 | 28 | React.useEffect(() => { 29 | load(); 30 | }, []); 31 | 32 | async function load() { 33 | let unitEnum: UnitsEnum = Number(await Global.GetStorage(Global.STORAGE_SETTINGS_UNIT)); 34 | if (unitEnum) { 35 | setUnits(unitEnum); 36 | } 37 | let emailSettings = new Map(); 38 | if(data.user.userSettings.emailLike) { 39 | emailSettings.set(SettingsEmailEnum.LIKE, true); 40 | } else { 41 | emailSettings.set(SettingsEmailEnum.LIKE, false); 42 | } 43 | if(data.user.userSettings.emailChat) { 44 | emailSettings.set(SettingsEmailEnum.CHAT, true); 45 | } else { 46 | emailSettings.set(SettingsEmailEnum.CHAT, false); 47 | } 48 | setEmailSettings(emailSettings); 49 | } 50 | 51 | async function updateUnits(num: number) { 52 | setUnits(num); 53 | await Global.Fetch(Global.format(URL.USER_UPDATE_UNITS, String(num)), 'post'); 54 | await Global.SetStorage(Global.STORAGE_SETTINGS_UNIT, String(num)); 55 | } 56 | 57 | async function updateEmailSettings(id: number, checked: boolean) { 58 | emailSettings.set(id, checked); 59 | setEmailSettings(emailSettings); 60 | let value = checked ? URL.PATH_BOOLEAN_TRUE : URL.PATH_BOOLEAN_FALSE; 61 | if (id === SettingsEmailEnum.LIKE) { 62 | Global.Fetch(Global.format(URL.USER_SETTING_EMAIL_LIKE, value), 'post'); 63 | data.user.userSettings.emailLike = checked; 64 | } else if (id === SettingsEmailEnum.CHAT) { 65 | Global.Fetch(Global.format(URL.USER_SETTING_EMAIL_CHAT, value), 'post'); 66 | data.user.userSettings.emailChat = checked; 67 | } 68 | } 69 | 70 | return ( 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 90 | 91 | 92 | item[1]).map((item) => item[0])} 98 | onValueChanged={updateEmailSettings}> 99 | 100 | 101 | 102 | 103 | 104 | ); 105 | }; 106 | 107 | export default Settings; 108 | -------------------------------------------------------------------------------- /screens/Donate.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | TouchableOpacity, 5 | FlatList, 6 | RefreshControl, 7 | useWindowDimensions 8 | } from "react-native"; 9 | 10 | import { Text, Button, Menu, ActivityIndicator, useTheme } from "react-native-paper"; 11 | import { CardItemDonate } from "../components"; 12 | import styles, { STATUS_BAR_HEIGHT } from "../assets/styles"; 13 | import * as I18N from "../i18n"; 14 | import * as Global from "../Global"; 15 | import * as URL from "../URL"; 16 | import { DonationDtoListModel, DonationDto, RootStackParamList } from "../myTypes"; 17 | import * as Linking from 'expo-linking'; 18 | import VerticalView from "../components/VerticalView"; 19 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 20 | 21 | type Props = BottomTabScreenProps 22 | 23 | const Donate = ({route: _r, navigation: _n}: Props) => { 24 | 25 | const { colors } = useTheme(); 26 | 27 | const FILTER_RECENT = 1; 28 | const FILTER_AMOUNT = 2; 29 | const topBarHeight = 62; 30 | 31 | const i18n = I18N.getI18n(); 32 | const { height, width } = useWindowDimensions(); 33 | 34 | const [refreshing] = React.useState(false); // todo: setRefreshing 35 | const [results, setResults] = React.useState(Array); 36 | const [filter, setFilter] = React.useState(FILTER_RECENT); 37 | const [loading, setLoading] = React.useState(false); 38 | 39 | const [menuSortVisible, setMenuSortVisible] = React.useState(false); 40 | 41 | const showMenuSort = () => setMenuSortVisible(true); 42 | const hideMenuSort = () => setMenuSortVisible(false); 43 | 44 | async function load() { 45 | setLoading(true); 46 | let response = await Global.Fetch(Global.format(URL.API_DONATE_RECENT, filter)); 47 | let data: DonationDtoListModel = response.data; 48 | setResults(data.list); 49 | setLoading(false); 50 | } 51 | 52 | function updateFilter(num: number) { 53 | if (num !== filter) { 54 | setFilter(num); 55 | } 56 | hideMenuSort(); 57 | } 58 | 59 | React.useEffect(() => { 60 | load(); 61 | }, [filter]); 62 | 63 | React.useEffect(() => { 64 | load(); 65 | }, []); 66 | 67 | return ( 68 | 69 | {loading && 70 | 71 | 72 | 73 | } 74 | 75 | 76 | 77 | {Global.FLAG_ENABLE_DONATION && 78 | } 81 | 82 | showMenuSort()}> 86 | {i18n.t('sort')} 87 | }> 88 | { updateFilter(FILTER_RECENT) }} title={i18n.t('donate.filter.recent')} /> 89 | { updateFilter(FILTER_AMOUNT) }} title={i18n.t('donate.filter.amount')} /> 90 | 91 | 92 | 93 | 94 | 95 | } 98 | columnWrapperStyle={{ flex: 1, justifyContent: "space-around" }} 99 | numColumns={2} 100 | data={results} 101 | keyExtractor={(item, index) => index.toString()} 102 | renderItem={({ item }) => ( 103 | 104 | 108 | 109 | )} 110 | /> 111 | 112 | 113 | ) 114 | }; 115 | 116 | export default Donate; 117 | -------------------------------------------------------------------------------- /URL.tsx: -------------------------------------------------------------------------------- 1 | //export const DOMAIN : string = "http://localhost:8080" 2 | //export const DOMAIN : string = "https://beta.alovoa.com" 3 | export const DOMAIN : string = "https://alovoa.com" 4 | 5 | export const PATH_BOOLEAN_TRUE = "true" 6 | export const PATH_BOOLEAN_FALSE = "false" 7 | 8 | export const IMPRINT = DOMAIN + "/imprint" 9 | export const PRIVACY = DOMAIN + "/privacy" 10 | export const TOS = DOMAIN + "/tos" 11 | export const DONATE_LIST = DOMAIN + "/donate-list" 12 | 13 | export const AUTH_LOGIN = DOMAIN + "/login" 14 | export const AUTH_LOGOUT = DOMAIN + "/logout" 15 | export const AUTH_LOGIN_ERROR = DOMAIN + "/?auth-error" 16 | 17 | export const AUTH_GOOGLE = DOMAIN + "/oauth2/authorization/google" 18 | export const AUTH_FACEBOOK = DOMAIN + "/oauth2/authorization/facebook" 19 | export const AUTH_COOKIE = DOMAIN + "/oauth2/remember-me-cookie/%s/%s" 20 | 21 | export const API_RESOURCE_YOUR_PROFILE = DOMAIN + "/api/v1/resource/profile" 22 | export const API_RESOURCE_PROFILE = DOMAIN + "/api/v1/resource/profile/view/%s" 23 | export const API_RESOURCE_SEARCH = DOMAIN + "/api/v1/resource/search" 24 | export const API_RESOURCE_ALERTS = DOMAIN + "/api/v1/resource/alerts" 25 | export const API_RESOURCE_CHATS = DOMAIN + "/api/v1/resource/chats" 26 | export const API_RESOURCE_CHATS_DETAIL = DOMAIN + "/api/v1/resource/chats/%s" 27 | export const API_RESOURCE_DONATE = DOMAIN + "/api/v1/resource/donate" 28 | export const API_RESOURCE_USER_ONBOARDING = DOMAIN + "/api/v1/resource/user/onboarding" 29 | export const API_RESOURCE_USER_BLOCKED = DOMAIN + "/api/v1/resource/blocked-users" 30 | export const API_RESOURCE_USER_LIKED = DOMAIN + "/api/v1/resource/liked-users" 31 | export const API_RESOURCE_USER_HIDDEN = DOMAIN + "/api/v1/resource/disliked-users" 32 | export const API_SEARCH = DOMAIN + "/api/v1/search/users" 33 | export const API_DONATE_RECENT = DOMAIN + "/api/v1/donate/recent/%s"; 34 | export const API_MESSAGE_UPDATE = DOMAIN + "/api/v1/message/update/%s/%s"; 35 | 36 | export const CATPCHA_GENERATE = DOMAIN + "/captcha/generate"; 37 | 38 | export const PASSWORD_RESET = DOMAIN + "/password/reset" 39 | 40 | export const REGISTER = DOMAIN + "/register"; 41 | export const REGISTER_OAUTH = DOMAIN + "/register-oauth"; 42 | 43 | export const USER_INTEREST_AUTOCOMPLETE = DOMAIN + "/user/interest/autocomplete/%s"; 44 | export const USER_ONBOARDING = DOMAIN + "/user/onboarding"; 45 | export const USER_STATUS_ALERT = DOMAIN + "/user/status/new-alert" 46 | export const USER_STATUS_ALERT_LANG = DOMAIN + "/user/status/new-alert/%s" 47 | export const USER_STATUS_MESSAGE = DOMAIN + "/user/status/new-message" 48 | 49 | export const USER_UPDATE_PROFILE_PICTURE = DOMAIN + "/user/update/profile-picture" 50 | export const USER_UPDATE_DESCRIPTION = DOMAIN + "/user/update/description" 51 | export const USER_UPDATE_INTENTION = DOMAIN + "/user/update/intention/%s" 52 | export const USER_UPDATE_MIN_AGE = DOMAIN + "/user/update/min-age/%s" 53 | export const USER_UPDATE_MAX_AGE = DOMAIN + "/user/update/max-age/%s" 54 | export const USER_UPDATE_PREFERED_GENDER = DOMAIN + "/user/update/preferedGender/%s/%s" 55 | export const USER_UPDATE_MISC_INFO = DOMAIN + "/user/update/misc-info/%s/%s" 56 | export const USER_ADD_INTEREST = DOMAIN + "/user/interest/add/%s" 57 | export const USER_REMOVE_INTEREST = DOMAIN + "/user/interest/delete/%s" 58 | export const USER_UPDATE_UNITS = DOMAIN + "/user/units/update/%s" 59 | export const USER_USERDATA = DOMAIN + "/user/userdata/%s"; 60 | export const USER_DELETE_ACCOUNT = DOMAIN + "/user/delete-account"; 61 | 62 | export const USER_LIKE = DOMAIN + "/user/like/%s" 63 | export const USER_LIKE_MESSAGE = DOMAIN + "/user/like/%s/%s" 64 | export const USER_HIDE = DOMAIN + "/user/hide/%s" 65 | export const USER_BLOCK = DOMAIN + "/user/block/%s" 66 | export const USER_UNBLOCK = DOMAIN + "/user/unblock/%s" 67 | export const USER_REPORT = DOMAIN + "/user/report/%s" 68 | export const USER_ADD_IMAGE = DOMAIN + "/user/image/add" 69 | export const USER_DELETE_IMAGE = DOMAIN + "/user/image/delete/%s" 70 | 71 | export const USER_PROMPT_DELETE = DOMAIN + "/user/prompt/delete/%s" 72 | export const USER_PROMPT_ADD = DOMAIN + "/user/prompt/add" 73 | export const USER_PROMPT_UPDATE = DOMAIN + "/user/prompt/update" 74 | 75 | export const USER_SETTING_EMAIL_LIKE = DOMAIN + "/user/settings/emailLike/update/%s" 76 | export const USER_SETTING_EMAIL_CHAT = DOMAIN + "/user/settings/emailChat/update/%s" 77 | 78 | export const USER_UPDATE_LOCATION = DOMAIN + "/user/update/location/%s/%s" 79 | 80 | export const USER_UPDATE_VERIFICATION_PICTURE = DOMAIN + "/user/update/verification-picture" 81 | export const USER_UPDATE_VERIFICATION_PICTURE_UPVOTE = DOMAIN + "/user/update/verification-picture/upvote/%s" 82 | export const USER_UPDATE_VERIFICATION_PICTURE_DOWNVOTE = DOMAIN + "/user/update/verification-picture/downvote/%s" 83 | 84 | export const MESSAGE_SEND = DOMAIN + "/message/send/%s"; -------------------------------------------------------------------------------- /components/AgeRangeSliderModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GRAY, WIDESCREEN_HORIZONTAL_MAX } from "../assets/styles"; 3 | import { RangeSliderModalT } from "../myTypes"; 4 | import { Modal, Portal, Text, Button, useTheme, IconButton } from 'react-native-paper'; 5 | import { View, useWindowDimensions } from "react-native"; 6 | import Slider from "@react-native-community/slider"; 7 | 8 | const AgeRangeSliderModal = ({ 9 | title, 10 | titleLower, 11 | titleUpper, 12 | valueLower = 0, 13 | valueUpper = 0, 14 | onValueLowerChanged, 15 | onValueUpperChanged }: RangeSliderModalT) => { 16 | 17 | const MIN_AGE = 18; 18 | const MAX_AGE = 100; 19 | 20 | const { colors } = useTheme(); 21 | const { width } = useWindowDimensions(); 22 | 23 | const [minAgeText, setMinAgeText] = React.useState(MIN_AGE) 24 | const [maxAgeText, setMaxAgeText] = React.useState(MAX_AGE) 25 | const [lowerValue, setLowerValue] = React.useState(valueLower); 26 | const [upperValue, setUpperValue] = React.useState(valueUpper); 27 | const [buttonText, setButtonText] = React.useState(""); 28 | const [visible, setVisible] = React.useState(false); 29 | const showModal = () => setVisible(true); 30 | const hideModal = () => setVisible(false); 31 | const containerStyle = { backgroundColor: colors.background, padding: 24, marginHorizontal: calcMarginModal(), borderRadius: 8 }; 32 | 33 | function calcMarginModal() { 34 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 35 | } 36 | 37 | function updateButtonText() { 38 | let text = lowerValue.toString() + " - " + upperValue.toString(); 39 | setButtonText(text); 40 | } 41 | 42 | React.useEffect(() => { 43 | setLowerValue(valueLower) 44 | setMinAgeText(valueLower); 45 | }, [valueLower]); 46 | 47 | React.useEffect(() => { 48 | setUpperValue(valueUpper); 49 | setMaxAgeText(valueUpper); 50 | }, [valueUpper]); 51 | 52 | React.useEffect(() => { 53 | updateButtonText(); 54 | }, [lowerValue, upperValue]); 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 67 | 68 | {title} 69 | 70 | 71 | 72 | 73 | {titleLower}: 74 | {minAgeText} 75 | 76 | { 85 | setMinAgeText(value); 86 | }} 87 | onSlidingComplete={(value: number) => { 88 | onValueLowerChanged(value); 89 | }} 90 | /> 91 | 92 | 93 | 94 | {titleUpper}: 95 | {maxAgeText} 96 | 97 | { 106 | setMaxAgeText(value); 107 | }} 108 | onSlidingComplete={(value: number) => { 109 | onValueUpperChanged(value); 110 | }} 111 | /> 112 | 113 | 114 | 115 | 116 | 117 | {title} 118 | 120 | 121 | ); 122 | }; 123 | 124 | export default AgeRangeSliderModal; 125 | -------------------------------------------------------------------------------- /components/InterestView.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { InterestModalT, UserInterest, UserInterestAutocomplete, UserInterestDto } from "../myTypes"; 3 | import { Text, Button, Searchbar } from 'react-native-paper'; 4 | import { Keyboard, View, useWindowDimensions } from "react-native"; 5 | import * as Global from "../Global"; 6 | import * as URL from "../URL"; 7 | import * as I18N from "../i18n"; 8 | import { debounce } from "lodash"; 9 | import { ScrollView } from "react-native-gesture-handler"; 10 | 11 | const InterestModal = ({ user, data, updateButtonText, setInterestsExternal }: InterestModalT) => { 12 | 13 | const i18n = I18N.getI18n(); 14 | const { height } = useWindowDimensions(); 15 | 16 | const [interests, setInterests] = React.useState(data); 17 | const [interest, setInterest] = React.useState(""); 18 | const [interestDebounce, setInterestDebounce] = React.useState(""); 19 | // const [loading, setLoading] = React.useState(false); 20 | const [suggestionsList, setSuggestionsList] = React.useState(Array); 21 | 22 | const interestRef = React.useRef(interestDebounce); 23 | const debounceInterestHandler = React.useCallback(debounce(getSuggestions, 700), []); 24 | 25 | React.useEffect(() => { 26 | interestRef.current = interestDebounce; 27 | debounceInterestHandler(); 28 | }, [interestDebounce]); 29 | 30 | React.useEffect(() => { 31 | setInterestDebounce(interest); 32 | }, [interest]); 33 | 34 | React.useEffect(() => { 35 | setInterests(data); 36 | if (setInterestsExternal) setInterestsExternal(data); 37 | if (updateButtonText) { 38 | updateButtonText(data); 39 | } 40 | }, [data]); 41 | 42 | React.useEffect(() => { 43 | if (updateButtonText) { 44 | updateButtonText(interests); 45 | } 46 | }, [interests]); 47 | 48 | async function getSuggestions() { 49 | let q = interestRef.current; 50 | let filterToken = cleanInterest(q); 51 | if (typeof q !== 'string' || q.length < 2) { 52 | setSuggestionsList([]) 53 | return; 54 | } 55 | // setLoading(true) 56 | const response = await Global.Fetch(Global.format(URL.USER_INTEREST_AUTOCOMPLETE, encodeURI(filterToken))); 57 | const items: UserInterestAutocomplete[] = response.data; 58 | const suggestions: UserInterestDto[] = items.map(item => { 59 | return { id: item.name, number: item.name + " (" + item.countString + ")" } 60 | }); 61 | 62 | setSuggestionsList(suggestions) 63 | // setLoading(false); 64 | }; 65 | 66 | async function addInterest(interest: string) { 67 | if (interest) { 68 | interest = cleanInterest(interest); 69 | if (user) await Global.Fetch(Global.format(URL.USER_ADD_INTEREST, interest), 'post'); 70 | let newInterest: UserInterest = { text: interest }; 71 | const copy = [...interests]; 72 | copy.push(newInterest); 73 | setInterests(copy); 74 | if (setInterestsExternal) setInterestsExternal(copy); 75 | setInterest(""); 76 | Keyboard.dismiss(); 77 | if (user) user.interests = copy; 78 | } 79 | } 80 | 81 | async function removeInterest(interest: UserInterest, index: number) { 82 | if (user) await Global.Fetch(Global.format(URL.USER_REMOVE_INTEREST, interest.text), 'post'); 83 | let interestsCopy = [...interests]; 84 | interestsCopy.splice(index, 1); 85 | setInterests(interestsCopy); 86 | if (setInterestsExternal) setInterestsExternal(interestsCopy); 87 | if (user) user.interests = interestsCopy; 88 | } 89 | 90 | function cleanInterest(txt: string) { 91 | let txtCopy = txt 92 | if (txtCopy) { 93 | txtCopy = txtCopy.replace(/ /g, "-"); 94 | let text = txtCopy.replace(/[^a-zA-Z0-9-]/g, '').toLowerCase(); 95 | return text; 96 | } 97 | return txt; 98 | } 99 | 100 | return ( 101 | 102 | 103 | {interests.length < Global.MAX_INTERESTS && 104 | { setInterest(text) }} 108 | onSubmitEditing={() => addInterest(interest)} 109 | autoCorrect={false} 110 | style={{ marginBottom: 18 }} 111 | /> 112 | } 113 | { 114 | suggestionsList.map((item, index) => ( 115 | 118 | )) 119 | } 120 | {suggestionsList?.length === 0 && {i18n.t('profile.onboarding.interests')}} 121 | 500 ? 240 : 80 }}> 122 | {suggestionsList?.length === 0 && 123 | interests.map((item, index) => ( 124 | 127 | )) 128 | } 129 | 130 | 131 | 132 | ); 133 | }; 134 | 135 | export default InterestModal; 136 | -------------------------------------------------------------------------------- /screens/MessageDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | RefreshControl, 5 | KeyboardAvoidingView, 6 | Keyboard, 7 | Image, 8 | ScrollView, 9 | useWindowDimensions, 10 | Platform 11 | } from "react-native"; 12 | import { 13 | TextInput, Card, MaterialBottomTabScreenProps 14 | } from "react-native-paper"; 15 | import { useTheme, Text } from "react-native-paper"; 16 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 17 | import Autolink, { CustomMatcher } from 'react-native-autolink'; 18 | import { MessageDtoListModel, MessageDto, RootStackParamList } from "../myTypes"; 19 | import styles from "../assets/styles"; 20 | import * as Global from "../Global"; 21 | import * as URL from "../URL"; 22 | import * as I18N from "../i18n"; 23 | 24 | const i18n = I18N.getI18n() 25 | const SECOND_MS = 1000; 26 | const POLL_MESSAGE = 5 * SECOND_MS; 27 | 28 | type Props = MaterialBottomTabScreenProps 29 | const MessageDetail = ({ route, navigation }: Props) => { 30 | 31 | const { conversation } = route.params; 32 | const insets = useSafeAreaInsets(); 33 | 34 | const { colors } = useTheme(); 35 | const { height, width } = useWindowDimensions(); 36 | const [refreshing] = React.useState(false); // todo: setRefreshing 37 | const [results, setResults] = React.useState(Array); 38 | let scrollViewRef = React.useRef(null); 39 | const [text, setText] = React.useState(""); 40 | 41 | const PhoneMatcher: CustomMatcher = { 42 | pattern: 43 | /(?<=^|\s|\.)[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{0,6}(?=$|\s|\.)/gm, 44 | type: 'phone-intl', 45 | getLinkUrl: ([number]) => `tel:${number}`, 46 | }; 47 | 48 | let messageUpdateInterval: NodeJS.Timeout | null = null; 49 | 50 | React.useEffect(() => { 51 | const unsubscribe = navigation.addListener('beforeRemove', () => { 52 | if (messageUpdateInterval) { 53 | clearInterval(messageUpdateInterval); 54 | } 55 | }); 56 | return unsubscribe; 57 | }, [navigation]); 58 | 59 | React.useEffect(() => { 60 | navigation.setOptions({ 61 | title: conversation.userName, tabBarIcon: () => ( 62 | 63 | ) 64 | }); 65 | load(); 66 | messageUpdateInterval = setInterval(() => { 67 | reloadMessages(false); 68 | }, POLL_MESSAGE); 69 | }, []); 70 | 71 | React.useEffect(() => { 72 | scrollToEnd(); 73 | }, [results]); 74 | 75 | function scrollToEnd() { 76 | setTimeout(function () { 77 | scrollViewRef?.current?.scrollToEnd(); 78 | }, 100); 79 | } 80 | 81 | async function load() { 82 | reloadMessages(true); 83 | } 84 | 85 | async function reloadMessages(first: boolean) { 86 | let firstVal = first ? "1" : "0"; 87 | let response = await Global.Fetch(Global.format(URL.API_MESSAGE_UPDATE, conversation.id, firstVal)); 88 | let data: MessageDtoListModel = response.data; 89 | if (data.list) { 90 | setResults(data.list); 91 | } 92 | } 93 | 94 | async function sendMessage() { 95 | const textCopy = text; 96 | setText(""); 97 | Keyboard.dismiss(); 98 | await Global.Fetch(Global.format(URL.MESSAGE_SEND, conversation.id), 'post', textCopy, 'text/plain'); 99 | reloadMessages(false); 100 | } 101 | 102 | const styleYourChat = { 103 | color: 'white', 104 | backgroundColor: colors.primary 105 | } 106 | 107 | const styleChat = { 108 | marginLeft: 4, 109 | marginRight: 4, 110 | marginBottom: 6, 111 | padding: 10, 112 | borderRadius: 10, 113 | maxWidth: width * 0.85, 114 | } 115 | 116 | return ( 117 | 118 | }> 123 | { 124 | results.map((item, index) => ( 125 | 126 | 127 | {} 128 | 129 | 130 | )) 131 | } 132 | 133 | 135 | setText(text)} 141 | onSubmitEditing={sendMessage} 142 | placeholder={i18n.t('chat.placeholder')} 143 | right={ sendMessage()} icon="send" />}> 144 | 145 | 146 | ) 147 | }; 148 | 149 | export default MessageDetail; 150 | -------------------------------------------------------------------------------- /assets/images/search-empty.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ColorModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles, { WIDESCREEN_HORIZONTAL_MAX } from "../assets/styles"; 3 | import { Modal, Portal, Text, Button, useTheme, IconButton } from 'react-native-paper'; 4 | import { ScrollView, View, useWindowDimensions } from "react-native"; 5 | import * as I18N from "../i18n"; 6 | import * as Global from "../Global"; 7 | import ColorPicker, { HueSlider, returnedResults, BrightnessSlider } from 'reanimated-color-picker'; 8 | 9 | const ColorModal = ({ title }: any) => { 10 | 11 | const { colors } = useTheme(); 12 | const { width } = useWindowDimensions(); 13 | const i18n = I18N.getI18n(); 14 | 15 | const [primary, setPrimary] = React.useState(Global.DEFAULT_COLOR_PRIMARY); 16 | const [secondary, setSecondary] = React.useState(Global.DEFAULT_COLOR_SECONDARY); 17 | const [changed, setChanged] = React.useState(false); 18 | const [visible, setVisible] = React.useState(false); 19 | const [loading, setLoading] = React.useState(true); 20 | const showModal = () => setVisible(true); 21 | const hideModal = () => setVisible(false); 22 | const containerStyle = { backgroundColor: colors.background, padding: 24, marginHorizontal: calcMarginModal(), borderRadius: 8 }; 23 | 24 | function calcMarginModal() { 25 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 26 | } 27 | 28 | React.useEffect(() => { 29 | load(); 30 | }, []); 31 | 32 | React.useEffect(() => { 33 | if (changed) { 34 | Global.ShowToast(i18n.t('restart-apply-changes')); 35 | } 36 | }, [changed]); 37 | 38 | async function load() { 39 | await loadStoredColors(); 40 | setLoading(false); 41 | } 42 | 43 | async function loadStoredColors() { 44 | let pColor = await Global.GetStorage(Global.STORAGE_SETTINGS_COLOR_PRIMARY); 45 | if (pColor) { 46 | await changePrimaryColor(pColor); 47 | } 48 | 49 | let sColor = await Global.GetStorage(Global.STORAGE_SETTINGS_COLOR_SECONDARY); 50 | if (sColor) { 51 | await changeSecondaryColor(sColor); 52 | } 53 | } 54 | 55 | function primaryColorChanged(color: returnedResults) { 56 | changePrimaryColor(color.hex) 57 | } 58 | 59 | function secondaryColorChanged(color: returnedResults) { 60 | changeSecondaryColor(color.hex); 61 | } 62 | 63 | function resetPrimaryColor() { 64 | changePrimaryColor(Global.DEFAULT_COLOR_PRIMARY); 65 | } 66 | 67 | function resetSecondaryColor() { 68 | changeSecondaryColor(Global.DEFAULT_COLOR_SECONDARY); 69 | } 70 | 71 | async function changePrimaryColor(hex: string) { 72 | setPrimary(hex); 73 | await Global.SetStorage(Global.STORAGE_SETTINGS_COLOR_PRIMARY, hex); 74 | if (!loading) { 75 | setChanged(true); 76 | } 77 | } 78 | 79 | async function changeSecondaryColor(hex: string) { 80 | setSecondary(hex); 81 | await Global.SetStorage(Global.STORAGE_SETTINGS_COLOR_SECONDARY, hex); 82 | if (!loading) { 83 | setChanged(true); 84 | } 85 | } 86 | 87 | return ( 88 | 89 | 90 | 91 | 92 | 98 | 99 | {title} 100 | 101 | 102 | 103 | {i18n.t('profile.settings.colors.primary')} 104 | 105 | 106 | 107 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {i18n.t('profile.settings.colors.secondary')} 123 | 124 | 125 | 126 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | {i18n.t('profile.settings.colors.title')} 142 | 148 | 149 | ); 150 | }; 151 | 152 | export default ColorModal; 153 | -------------------------------------------------------------------------------- /assets/monochrome-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 63 | 65 | 66 | 68 | image/svg+xml 69 | 71 | 72 | 73 | 74 | 80 | 86 | 92 | 98 | 104 | 110 | 116 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /screens/PasswordReset.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTheme, Text, Button, Dialog, TextInput, IconButton } from "react-native-paper"; 3 | import { View, StyleSheet, Image, useWindowDimensions } from "react-native"; 4 | import * as WebBrowser from 'expo-web-browser'; 5 | import * as Global from "../Global"; 6 | import * as URL from "../URL"; 7 | import * as I18N from "../i18n"; 8 | import SvgPasswordReset from "../assets/images/password-reset.svg"; 9 | import { Captcha, PasswordResetDto, RootStackParamList } from "../myTypes"; 10 | import VerticalView from "../components/VerticalView"; 11 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 12 | 13 | const i18n = I18N.getI18n() 14 | const IMAGE_HEADER = "data:image/webp;base64,"; 15 | 16 | WebBrowser.maybeCompleteAuthSession(); 17 | 18 | type Props = BottomTabScreenProps 19 | 20 | const PasswordReset = ({ route, navigation }: Props) => { 21 | 22 | const { colors } = useTheme(); 23 | 24 | const svgHeight = 150; 25 | const svgWidth = 200; 26 | const { height, width } = useWindowDimensions(); 27 | 28 | const [email, setEmail] = React.useState(""); 29 | const [emailValid, setEmailValid] = React.useState(false); 30 | const [captchaId, setCaptchaId] = React.useState(0); 31 | const [captchaImage, setCaptchaImage] = React.useState(""); 32 | const [captchaText, setCaptchaText] = React.useState(""); 33 | 34 | //vars for dialog 35 | const [visible, setVisible] = React.useState(false); 36 | const showDialog = () => setVisible(true); 37 | const hideDialog = () => setVisible(false); 38 | 39 | const styles = StyleSheet.create({ 40 | container: { flex: 1, backgroundColor: 'white' }, 41 | child: { width, justifyContent: 'center' }, 42 | text: { fontSize: width * 0.5, textAlign: 'center' }, 43 | view: { 44 | width: width, 45 | height: height, 46 | justifyContent: 'center', 47 | alignItems: 'center' 48 | }, 49 | button: { 50 | alignItems: 'center', 51 | justifyContent: 'center', 52 | paddingVertical: 12, 53 | paddingHorizontal: 32, 54 | borderRadius: 4, 55 | elevation: 3, 56 | margin: 4, 57 | flexDirection: 'row', 58 | }, 59 | svg: { 60 | marginTop: 24, 61 | marginBottom: 12 62 | }, 63 | profilePicButton: { 64 | width: 200, 65 | height: 200 66 | }, 67 | title: { 68 | textAlign: 'center', 69 | marginTop: 12, 70 | marginBottom: 12, 71 | fontSize: 18, 72 | }, 73 | radioButton: { 74 | marginBottom: 12, 75 | marginTop: 12, 76 | }, 77 | switchText: { 78 | marginBottom: 12, 79 | marginTop: 12, 80 | }, 81 | warning: { 82 | textAlign: 'center', 83 | marginTop: 24, 84 | opacity: 0.5, 85 | fontSize: 10 86 | }, 87 | buttonText: { 88 | color: 'white' 89 | }, 90 | }); 91 | 92 | React.useEffect(() => { 93 | navigation.setOptions({ 94 | title: '' 95 | }); 96 | }, []); 97 | 98 | async function showCaptchaDialog() { 99 | if (emailValid) { 100 | setCaptchaText(""); 101 | let res = await Global.Fetch(URL.CATPCHA_GENERATE); 102 | let captcha: Captcha = res.data; 103 | setCaptchaId(captcha.id); 104 | setCaptchaImage(IMAGE_HEADER + captcha.image); 105 | showDialog(); 106 | } 107 | } 108 | 109 | async function resetPassword() { 110 | if (emailValid && captchaId && captchaText) { 111 | setCaptchaText(""); 112 | let data: PasswordResetDto = { captchaId: captchaId, captchaText: captchaText, email: email } 113 | try { 114 | await Global.Fetch(URL.PASSWORD_RESET, 'post', data); 115 | Global.ShowToast(i18n.t('password-reset-success')); 116 | Global.navigate("Login"); 117 | } catch (e) { 118 | console.error(e); 119 | hideDialog(); 120 | Global.ShowToast(i18n.t('error.generic')); 121 | } 122 | } 123 | } 124 | 125 | return ( 126 | 127 | {i18n.t('password-reset')} 128 | 129 | 130 | 131 | 132 | 133 | { 138 | setEmail(text); 139 | setEmailValid(Global.isEmailValid(text)); 140 | }} 141 | keyboardType="email-address" 142 | autoCapitalize="none" 143 | /> 144 | 145 | 147 | 148 | 149 | {i18n.t('captcha.title')} 150 | 151 | 152 | setCaptchaText(text)} 157 | /> 158 | 159 | 160 | { showCaptchaDialog() }} 165 | /> 166 | { resetPassword() }} 171 | /> 172 | 173 | 174 | 175 | ) 176 | }; 177 | 178 | export default PasswordReset; -------------------------------------------------------------------------------- /assets/adaptive-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 63 | 65 | 66 | 68 | image/svg+xml 69 | 71 | 72 | 73 | 74 | 80 | 83 | 89 | 95 | 101 | 107 | 113 | 119 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /screens/profile/AdvancedSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | useWindowDimensions 5 | } from "react-native"; 6 | import styles from "../../assets/styles"; 7 | import { RootStackParamList, UserDto } from "../../myTypes"; 8 | import * as I18N from "../../i18n"; 9 | import * as Global from "../../Global"; 10 | import * as URL from "../../URL"; 11 | import VerticalView from "../../components/VerticalView"; 12 | import { IconButton, TextInput, useTheme } from "react-native-paper"; 13 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 14 | 15 | const i18n = I18N.getI18n(); 16 | 17 | type Props = BottomTabScreenProps 18 | 19 | const AdvancedSettings = ({ route }: Props) => { 20 | 21 | const user: UserDto = route.params.user; 22 | const { colors } = useTheme(); 23 | const { height } = useWindowDimensions(); 24 | const defaultTimeoutString = String(Global.DEFAULT_GPS_TIMEOUT); 25 | const defaultHideThresholdString = String(Global.DEFAULT_HIDE_THRESHOLD); 26 | 27 | React.useEffect(() => { 28 | load(); 29 | }, []); 30 | 31 | const [latitude, setLatitude] = React.useState("0.00"); 32 | const [longitude, setLongitude] = React.useState("0.00"); 33 | const [gpsTimeout, setGpsTimeout] = React.useState(defaultTimeoutString); 34 | const [hideThreshold, setHideThreshold] = React.useState(defaultHideThresholdString); 35 | 36 | async function load() { 37 | setLatitude(String(user.locationLatitude)); 38 | setLongitude(String(user.locationLongitude)); 39 | let timeoutStorage = await Global.GetStorage(Global.STORAGE_ADV_SEARCH_GPSTIMEOPUT); 40 | setGpsTimeout(timeoutStorage ? timeoutStorage : defaultTimeoutString); 41 | 42 | let hideThresholdStorage = await Global.GetStorage(Global.STORAGE_ADV_SEARCH_HIDE_THRESHOLD); 43 | setHideThreshold(hideThresholdStorage ? hideThresholdStorage : defaultHideThresholdString); 44 | } 45 | 46 | async function uploadLocation() { 47 | if(!isNaN(Number(latitude)) && !isNaN(Number(longitude))) { 48 | Global.Fetch(Global.format(URL.USER_UPDATE_LOCATION, latitude, longitude), 'post'); 49 | } 50 | } 51 | 52 | async function saveGpsTimeout(value?: number) { 53 | if(value) { 54 | Global.SetStorage(Global.STORAGE_ADV_SEARCH_GPSTIMEOPUT, String(value)); 55 | } else if(!isNaN(Number(gpsTimeout))) { 56 | Global.SetStorage(Global.STORAGE_ADV_SEARCH_GPSTIMEOPUT, gpsTimeout); 57 | } 58 | } 59 | 60 | async function saveHideThreshold(value?: number) { 61 | if(value) { 62 | Global.SetStorage(Global.STORAGE_ADV_SEARCH_HIDE_THRESHOLD, String(value)); 63 | } else if(!isNaN(Number(hideThreshold))) { 64 | Global.SetStorage(Global.STORAGE_ADV_SEARCH_HIDE_THRESHOLD, hideThreshold); 65 | } 66 | } 67 | 68 | return ( 69 | 70 | 71 | 72 | 80 | 88 | 89 | 95 | 96 | 97 | 98 | saveGpsTimeout()} 104 | keyboardType="decimal-pad" 105 | /> 106 | 107 | { 112 | setGpsTimeout(defaultTimeoutString); 113 | saveGpsTimeout(Global.DEFAULT_GPS_TIMEOUT) 114 | }} 115 | /> 116 | saveGpsTimeout()} 121 | /> 122 | 123 | 124 | 125 | saveHideThreshold()} 131 | keyboardType="decimal-pad" 132 | /> 133 | 134 | { 139 | setHideThreshold(defaultHideThresholdString); 140 | saveHideThreshold(Global.DEFAULT_HIDE_THRESHOLD) 141 | }} 142 | /> 143 | saveHideThreshold()} 148 | /> 149 | 150 | 151 | 152 | 153 | ); 154 | }; 155 | 156 | export default AdvancedSettings; 157 | -------------------------------------------------------------------------------- /screens/profile/Pictures.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | TouchableOpacity, 5 | Image, 6 | StyleSheet, 7 | useWindowDimensions, 8 | FlatList 9 | } from "react-native"; 10 | import styles, { WIDESCREEN_HORIZONTAL_MAX } from "../../assets/styles"; 11 | import * as I18N from "../../i18n"; 12 | import * as Global from "../../Global"; 13 | import * as URL from "../../URL"; 14 | import { RootStackParamList, UserDto, UserImage, YourProfileResource } from "../../myTypes"; 15 | import { Badge, Button } from 'react-native-paper'; 16 | import Alert from "../../components/Alert"; 17 | import VerticalView from "../../components/VerticalView"; 18 | import { useHeaderHeight } from '@react-navigation/elements'; 19 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 20 | 21 | type Props = BottomTabScreenProps 22 | 23 | const Pictures = ({ route, navigation }: Props) => { 24 | 25 | const user: UserDto = route.params.user; 26 | 27 | const { height } = useWindowDimensions(); 28 | const headerHeight = useHeaderHeight(); 29 | const i18n = I18N.getI18n() 30 | const MAX_IMAGES = 4; 31 | 32 | const [alertVisible, setAlertVisible] = React.useState(false); 33 | const [profilePic, setProfilePic] = React.useState(""); 34 | const [images, setImages] = React.useState(Array); 35 | const [changedProfilePic, setChangedProfilePic] = React.useState(false); 36 | const [imageIdToBeRemoved, setImageIdToBeRemoved] = React.useState(0); 37 | 38 | const alertButtons = [ 39 | { 40 | text: i18n.t('cancel'), 41 | onPress: () => { 42 | setAlertVisible(false); 43 | setImageIdToBeRemoved(0); 44 | } 45 | }, 46 | { 47 | text: i18n.t('ok'), 48 | onPress: async () => { 49 | await Global.Fetch(Global.format(URL.USER_DELETE_IMAGE, String(imageIdToBeRemoved)), 'post'); 50 | let imagesCopy = [...images]; 51 | let newImages = imagesCopy.filter(item => item.id !== imageIdToBeRemoved); 52 | setImages(newImages); 53 | setImageIdToBeRemoved(0); 54 | setAlertVisible(false); 55 | user.images = newImages; 56 | } 57 | } 58 | ] 59 | 60 | React.useEffect(() => { 61 | if (imageIdToBeRemoved) { 62 | setAlertVisible(true); 63 | } 64 | }, [imageIdToBeRemoved]); 65 | 66 | React.useEffect( 67 | () => 68 | navigation.addListener('beforeRemove', (e: any) => { 69 | e.preventDefault(); 70 | goBack(); 71 | }), 72 | [navigation] 73 | ); 74 | 75 | React.useEffect(() => { 76 | setImages(user.images); 77 | setProfilePic(user.profilePicture); 78 | }, []); 79 | 80 | async function load() { 81 | let response = await Global.Fetch(URL.API_RESOURCE_YOUR_PROFILE); 82 | let data: YourProfileResource = response.data; 83 | let dto: UserDto = data.user; 84 | setImages(dto.images); 85 | setProfilePic(dto.profilePicture); 86 | } 87 | 88 | async function updateProfilePicture() { 89 | let imageData: string | null | undefined = await Global.pickImage(); 90 | if (imageData) { 91 | const bodyFormData = Global.buildFormData(imageData); 92 | await Global.Fetch(URL.USER_UPDATE_PROFILE_PICTURE, 'post', bodyFormData, 'multipart/form-data'); 93 | load(); 94 | setChangedProfilePic(true); 95 | navigation.setParams({ changed: false }); 96 | } 97 | } 98 | 99 | async function addImage() { 100 | let imageData: string | null | undefined = await Global.pickImage(); 101 | if (imageData != null) { 102 | const bodyFormData = Global.buildFormData(imageData); 103 | const response = await Global.Fetch(URL.USER_ADD_IMAGE, 'post', bodyFormData, 'multipart/form-data'); 104 | const responseImages: UserImage[] = response.data; 105 | setImages(responseImages); 106 | user.images = responseImages; 107 | } 108 | } 109 | 110 | async function removeImage(id: number) { 111 | setImageIdToBeRemoved(id); 112 | } 113 | 114 | async function goBack() { 115 | navigation.navigate('Main', { 116 | screen: Global.SCREEN_YOURPROFILE, 117 | params: { changed: changedProfilePic }, 118 | merge: true, 119 | }); 120 | } 121 | 122 | const style = StyleSheet.create({ 123 | image: { 124 | width: '100%', 125 | height: 'auto', 126 | maxWidth: WIDESCREEN_HORIZONTAL_MAX, 127 | aspectRatio: 1, 128 | }, 129 | imageSmall: { 130 | width: '50%', 131 | maxWidth: '50%', 132 | } 133 | }); 134 | 135 | return ( 136 | 137 | 141 | 146 | 147 | {images.length < MAX_IMAGES && 148 | 151 | } 152 | 153 | 154 | 155 | { updateProfilePicture() }}> 157 | 158 | 159 | 160 | 161 | 162 | 163 | index.toString()} 169 | renderItem={({ item }) => ( 170 | removeImage(item.id)}> 171 | 172 | 173 | )} 174 | /> 175 | 176 | 177 | 178 | 179 | ) 180 | }; 181 | 182 | export default Pictures; 183 | -------------------------------------------------------------------------------- /components/CardItemSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Image, TouchableOpacity, StyleProp, TextStyle, StyleSheet, useWindowDimensions, Platform, Pressable } from "react-native"; 3 | import { useTheme, Text, Chip, Button, Tooltip } from "react-native-paper"; 4 | import Icon from "./Icon"; 5 | import { CardItemT } from "../myTypes"; 6 | import * as Global from "../Global"; 7 | import styles, { 8 | DISLIKE_ACTIONS, 9 | LIKE_ACTIONS, 10 | GRAY, 11 | NAVIGATION_BAR_HEIGHT, 12 | WIDESCREEN_HORIZONTAL_MAX, 13 | STATUS_BAR_HEIGHT 14 | } from "../assets/styles"; 15 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 16 | import * as I18N from "../i18n"; 17 | 18 | const CardItem = ({ 19 | user, 20 | unitsImperial, 21 | swiper, 22 | onLikePressed, 23 | index 24 | }: CardItemT) => { 25 | 26 | const { colors } = useTheme(); 27 | const i18n = I18N.getI18n(); 28 | 29 | const { height, width } = useWindowDimensions(); 30 | const cardPadding = 8; 31 | 32 | const [showLikeTooltip, setShowLikeTooltip] = React.useState(false); 33 | const [tooManyReports, setTooManyReports] = React.useState(false); 34 | 35 | const style = StyleSheet.create({ 36 | image: { 37 | borderRadius: cardPadding, 38 | width: calcImageSize(), 39 | height: 'auto', 40 | maxWidth: WIDESCREEN_HORIZONTAL_MAX - cardPadding * 2, 41 | marginTop: cardPadding / 2 + STATUS_BAR_HEIGHT, 42 | marginBottom: cardPadding / 2, 43 | aspectRatio: 1, 44 | }, 45 | }); 46 | 47 | function calcImageSize(): number { 48 | if (width <= 380) { 49 | return width - (380 - width); 50 | } 51 | return width + 300 < height ? width - cardPadding * 2 : height / (height < 900 ? 2 : 1.5) - cardPadding * 2; 52 | } 53 | 54 | React.useEffect(() => { 55 | load(); 56 | }, []); 57 | 58 | async function load() { 59 | let hideThresholdString = await Global.GetStorage(Global.STORAGE_ADV_SEARCH_HIDE_THRESHOLD); 60 | let hideThreshold = hideThresholdString ? Number(hideThresholdString) : Global.DEFAULT_HIDE_THRESHOLD; 61 | setTooManyReports(user.numReports >= hideThreshold); 62 | } 63 | 64 | const nameStyle: StyleProp = [ 65 | { 66 | fontSize: 20, 67 | textAlign: 'auto', 68 | textAlignVertical: 'center' 69 | }, 70 | ]; 71 | 72 | React.useEffect(() => { 73 | showToolTip(); 74 | }, []); 75 | 76 | async function showToolTip() { 77 | if (index === 0) { 78 | let toolTip = await Global.GetStorage(Global.STORAGE_SEARCH_LIKE_TOOLTIP); 79 | if (!toolTip) { 80 | setShowLikeTooltip(true); 81 | Global.SetStorage(Global.STORAGE_SEARCH_LIKE_TOOLTIP, "1"); 82 | } 83 | } 84 | } 85 | 86 | function onLikeUser() { 87 | if (onLikePressed) { 88 | onLikePressed(); 89 | } 90 | setShowLikeTooltip(false); 91 | } 92 | 93 | function onHideUser() { 94 | swiper.current?.swipeLeft(); 95 | } 96 | 97 | return ( 98 | 99 | { tooManyReports && 100 | 101 | 102 | {Global.format(i18n.t('profile.search.report-card'), user.firstName)} 103 | 104 | 105 | 106 | 107 | 108 | 109 | } 110 | 111 | { !tooManyReports && 112 | Global.nagivateProfile(user)}> 113 | {/* IMAGE */} 114 | Global.nagivateProfile(user)}> 115 | 116 | 117 | 118 | {/* NAME */} 119 | 120 | 121 | {user.firstName + ", " + user.age} 122 | {user.lastActiveState <= 2 && } 123 | 124 | 125 | 126 | {user.distanceToUser} 127 | {unitsImperial ? ' mi' : ' km'} 128 | 129 | 130 | 131 | {/* COMMON INTERESTS */}{user.commonInterests.length > 0 && 132 | 133 | 134 | {i18n.t('profile.interests-common')} 135 | 136 | { 137 | user.commonInterests?.map((item, index) => ( 138 | {item.text} 139 | )) 140 | } 141 | 142 | 143 | 144 | } 145 | 146 | {/* DESCRIPTION */} 147 | { 148 | 149 | {user.description} 150 | 151 | } 152 | 153 | } 154 | 155 | {/* ACTIONS */} 156 | { !tooManyReports && 157 | 158 | onHideUser()}> 159 | 160 | 161 | 162 | onLikeUser()}> 163 | 164 | 165 | 166 | 167 | } 168 | 169 | ); 170 | }; 171 | 172 | export default CardItem; 173 | -------------------------------------------------------------------------------- /assets/styles/index.ts: -------------------------------------------------------------------------------- 1 | import Constants from "expo-constants"; 2 | import { StyleSheet, I18nManager } from "react-native"; 3 | 4 | export const WHITE = "#FFFFFF"; 5 | export const GRAY = "#757E90"; 6 | export const DARK_GRAY = "#363636"; 7 | export const BLACK = "#000000"; 8 | export const LINK = "#ec407a"; 9 | 10 | export const ONLINE_STATUS = "#46A575"; 11 | export const OFFLINE_STATUS = "#D04949"; 12 | 13 | export const LIKE_ACTIONS = WHITE; 14 | export const DISLIKE_ACTIONS = WHITE; 15 | 16 | export const STATUS_BAR_HEIGHT: number = Constants.statusBarHeight; 17 | export const NAVIGATION_BAR_HEIGHT = 80; 18 | export const WIDESCREEN_HORIZONTAL_MAX = 600; 19 | 20 | export default StyleSheet.create({ 21 | textInputAlign: { 22 | textAlign: I18nManager.isRTL ? "right" : "left", 23 | }, 24 | link: { 25 | color: LINK, 26 | flex: 1, 27 | }, 28 | center: { 29 | alignItems: "center", 30 | justifyContent: "center", 31 | }, 32 | radioButton: { 33 | marginBottom: 12, 34 | marginTop: 12, 35 | }, 36 | switchText: { 37 | marginBottom: 12, 38 | marginTop: 12, 39 | }, 40 | marginRight4: { 41 | marginRight: 4 42 | }, 43 | marginRight8: { 44 | marginRight: 8 45 | }, 46 | marginBottom4: { 47 | marginBottom: 4 48 | }, 49 | marginBottom8: { 50 | marginBottom: 18 51 | }, 52 | marginBottom12: { 53 | marginBottom: 12 54 | }, 55 | padding12: { 56 | padding: 12 57 | }, 58 | badge: { 59 | backgroundColor: 'red', 60 | marginBottom: -12, 61 | zIndex: 10 62 | }, 63 | // COMPONENT - CARD ITEM 64 | containerCardItem: { 65 | borderRadius: 8, 66 | alignItems: "center", 67 | margin: 4, 68 | elevation: 1, 69 | shadowOpacity: 0.05, 70 | shadowRadius: 10, 71 | shadowColor: GRAY, 72 | shadowOffset: { height: 0, width: 0 }, 73 | flexGrow: 1, 74 | // width: width - 8 75 | }, 76 | matchesTextCardItem: { 77 | color: WHITE, 78 | }, 79 | descriptionCardItem: { 80 | textAlign: I18nManager.isRTL ? "right" : "left", 81 | flexShrink: 1, 82 | opacity: 0.8, 83 | }, 84 | status: { 85 | paddingBottom: 10, 86 | flexDirection: "row", 87 | alignItems: "center", 88 | }, 89 | statusText: { 90 | color: GRAY, 91 | fontSize: 12, 92 | }, 93 | online: { 94 | width: 6, 95 | height: 6, 96 | backgroundColor: ONLINE_STATUS, 97 | borderRadius: 3, 98 | marginRight: 4, 99 | }, 100 | offline: { 101 | width: 6, 102 | height: 6, 103 | backgroundColor: OFFLINE_STATUS, 104 | borderRadius: 3, 105 | marginRight: 4, 106 | }, 107 | actionsCardItem: { 108 | flexDirection: "row", 109 | alignItems: "center", 110 | paddingBottom: 12, 111 | paddingTop: 12 112 | }, 113 | button: { 114 | width: 60, 115 | height: 60, 116 | borderRadius: 30, 117 | marginHorizontal: 7, 118 | alignItems: "center", 119 | justifyContent: "center", 120 | elevation: 1, 121 | shadowOpacity: 0.15, 122 | shadowRadius: 20, 123 | shadowColor: DARK_GRAY, 124 | shadowOffset: { height: 10, width: 0 }, 125 | }, 126 | miniButton: { 127 | width: 40, 128 | height: 40, 129 | borderRadius: 30, 130 | backgroundColor: WHITE, 131 | marginHorizontal: 7, 132 | alignItems: "center", 133 | justifyContent: "center", 134 | elevation: 1, 135 | shadowOpacity: 0.15, 136 | shadowRadius: 20, 137 | shadowColor: DARK_GRAY, 138 | shadowOffset: { height: 10, width: 0 }, 139 | }, 140 | 141 | // COMPONENT - CITY 142 | city: { 143 | backgroundColor: WHITE, 144 | padding: 10, 145 | borderRadius: 20, 146 | width: 100, 147 | elevation: 1, 148 | shadowOpacity: 0.05, 149 | shadowRadius: 10, 150 | shadowColor: BLACK, 151 | shadowOffset: { height: 0, width: 0 }, 152 | }, 153 | cityText: { 154 | color: DARK_GRAY, 155 | fontSize: 13, 156 | textAlign: "center", 157 | }, 158 | 159 | // COMPONENT - FILTERS 160 | filters: { 161 | backgroundColor: WHITE, 162 | padding: 4, 163 | borderRadius: 20, 164 | width: 90, 165 | elevation: 1, 166 | shadowOpacity: 0.05, 167 | shadowRadius: 10, 168 | shadowColor: BLACK, 169 | shadowOffset: { height: 0, width: 0 } 170 | 171 | }, 172 | filtersText: { 173 | color: DARK_GRAY, 174 | fontSize: 13, 175 | textAlign: "center", 176 | }, 177 | 178 | // COMPONENT - MESSAGE 179 | containerMessage: { 180 | flex: 1, 181 | alignItems: "center", 182 | justifyContent: "flex-start", 183 | flexDirection: "row", 184 | paddingHorizontal: 10, 185 | // width: width - 100, 186 | }, 187 | avatar: { 188 | borderRadius: 30, 189 | width: 60, 190 | height: 60, 191 | marginRight: 20, 192 | marginVertical: 15, 193 | }, 194 | message: { 195 | color: GRAY, 196 | fontSize: 12, 197 | paddingTop: 5, 198 | }, 199 | 200 | // COMPONENT - PROFILE ITEM 201 | containerProfileItem: { 202 | paddingHorizontal: 10, 203 | paddingBottom: 25, 204 | margin: 20, 205 | }, 206 | matchesTextProfileItem: { 207 | color: WHITE, 208 | textAlign: "center", 209 | }, 210 | name: { 211 | paddingTop: 25, 212 | paddingBottom: 5, 213 | fontSize: 24, 214 | textAlign: "center", 215 | }, 216 | descriptionProfileItem: { 217 | color: GRAY, 218 | textAlign: "center", 219 | paddingBottom: 20, 220 | fontSize: 13, 221 | }, 222 | info: { 223 | paddingVertical: 8, 224 | flexDirection: "row", 225 | alignItems: "center", 226 | }, 227 | iconProfile: { 228 | fontSize: 12, 229 | color: DARK_GRAY, 230 | paddingHorizontal: 10, 231 | }, 232 | infoContent: { 233 | color: GRAY, 234 | fontSize: 13, 235 | }, 236 | 237 | // CONTAINER - GENERAL 238 | bg: { 239 | flex: 1, 240 | resizeMode: "cover", 241 | // width: width, 242 | // height: height, 243 | }, 244 | top: { 245 | paddingTop: 12, 246 | paddingHorizontal: 10, 247 | flexDirection: "row", 248 | justifyContent: "space-between", 249 | alignItems: "center", 250 | }, 251 | title: { paddingBottom: 10, fontSize: 22 }, 252 | 253 | // CONTAINER - HOME 254 | containerHome: { 255 | marginHorizontal: 10, 256 | }, 257 | 258 | // CONTAINER - MATCHES 259 | containerMatches: { 260 | justifyContent: "space-around", 261 | flex: 1, 262 | }, 263 | 264 | // CONTAINER - MESSAGES 265 | containerMessages: { 266 | justifyContent: "space-between", 267 | flex: 1, 268 | paddingHorizontal: 10, 269 | }, 270 | 271 | // CONTAINER - PROFILE 272 | containerProfile: { marginHorizontal: 0 }, 273 | topIconLeft: { 274 | paddingLeft: 20, 275 | }, 276 | topIconRight: { 277 | paddingRight: 20, 278 | }, 279 | actionsProfile: { 280 | justifyContent: "center", 281 | flexDirection: "row", 282 | alignItems: "center", 283 | }, 284 | textButton: { 285 | fontSize: 15, 286 | color: WHITE, 287 | paddingLeft: 5, 288 | }, 289 | 290 | // MENU 291 | tabButtonText: { 292 | textTransform: "uppercase", 293 | }, 294 | iconMenu: { 295 | alignItems: "center", 296 | }, 297 | }); 298 | -------------------------------------------------------------------------------- /screens/profile/Prompts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { WIDESCREEN_HORIZONTAL_MAX } from "../../assets/styles"; 3 | import { Portal, Text, useTheme, IconButton, Surface, TextInput } from 'react-native-paper'; 4 | import { KeyboardAvoidingView, Pressable, View, useWindowDimensions } from "react-native"; 5 | import * as I18N from "../../i18n"; 6 | import * as Global from "../../Global"; 7 | import * as URL from "../../URL"; 8 | import { RootStackParamList, UserDto, UserPrompt } from "../../myTypes"; 9 | import Alert from "../../components/Alert"; 10 | import { useHeaderHeight } from '@react-navigation/elements'; 11 | import VerticalView from "../../components/VerticalView"; 12 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 13 | import Modal from "react-native-modal"; 14 | 15 | type Props = BottomTabScreenProps 16 | 17 | const Prompts = ({ route }: Props) => { 18 | 19 | const routeUserPrompts = route.params.prompts; 20 | const updatePrompts = route.params.updatePrompts; 21 | 22 | const { colors } = useTheme(); 23 | const { height, width } = useWindowDimensions(); 24 | const headerHeight = useHeaderHeight(); 25 | const i18n = I18N.getI18n(); 26 | const maxPrompts = 6; 27 | const maxPromptAmount = 20; 28 | const maxPromptTextLength = 120; 29 | const promptIdArray = Array.from({ length: maxPromptAmount }, (v, k) => k + 1);; 30 | 31 | enum ModalModeE { 32 | ADD = 1, 33 | EDIT = 2, 34 | DELETE = 3 35 | } 36 | 37 | const [visible, setVisible] = React.useState(false); 38 | const [userPrompts, setUserPrompts] = React.useState(routeUserPrompts); 39 | const [prompts, setPrompts] = React.useState>(new Map()); 40 | const [modalId, setModalId] = React.useState(0); 41 | const [modalText, setModalText] = React.useState(""); 42 | const [modalTitle, setModalTitle] = React.useState(""); 43 | const [modalMode, setModalMode] = React.useState(ModalModeE.ADD); 44 | const showModal = () => setVisible(true); 45 | const hideModal = () => setVisible(false); 46 | const [alertVisible, setAlertVisible] = React.useState(false); 47 | const containerStyle = { backgroundColor: colors.elevation.level3, padding: 24, marginHorizontal: calcMarginModal(), borderRadius: 8 }; 48 | 49 | const alertButtons = [ 50 | { 51 | text: i18n.t('cancel'), 52 | onPress: () => { setAlertVisible(false); }, 53 | }, 54 | { 55 | text: i18n.t('ok'), 56 | onPress: () => deletePrompt(modalId) 57 | } 58 | ] 59 | 60 | React.useEffect(() => { 61 | if (userPrompts) { 62 | let map = new Map(userPrompts.map((obj) => [obj.promptId, obj])); 63 | setPrompts(map); 64 | } 65 | }, [userPrompts]); 66 | 67 | React.useEffect(() => { 68 | updatePrompts([...prompts.values()]); 69 | }, [prompts]); 70 | 71 | function calcMarginModal() { 72 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 73 | } 74 | 75 | function openModal(mode: ModalModeE, prompt: UserPrompt,) { 76 | setModalId(prompt.promptId); 77 | setModalText(prompt.text); 78 | setModalTitle(i18n.t('profile.prompts.' + prompt.promptId)); 79 | setModalMode(mode); 80 | if (mode === ModalModeE.DELETE) { 81 | setAlertVisible(true); 82 | } else { 83 | showModal(); 84 | } 85 | } 86 | 87 | async function addPrompt(prompt: UserPrompt) { 88 | let copy = new Map(prompts); 89 | copy.set(prompt.promptId, prompt); 90 | setPrompts(copy); 91 | setUserPrompts(Array.from(copy.values())); 92 | hideModal(); 93 | await Global.Fetch(URL.USER_PROMPT_ADD, "post", prompt); 94 | } 95 | 96 | async function updatePrompt(prompt: UserPrompt) { 97 | let copy = new Map(prompts); 98 | copy.set(prompt.promptId, prompt); 99 | setPrompts(copy); 100 | setUserPrompts(Array.from(copy.values())); 101 | hideModal(); 102 | await Global.Fetch(URL.USER_PROMPT_UPDATE, "post", prompt); 103 | } 104 | 105 | async function deletePrompt(promptId: number) { 106 | let copy = new Map(prompts); 107 | copy.delete(promptId); 108 | setPrompts(copy); 109 | setUserPrompts(Array.from(copy.values())); 110 | setAlertVisible(false); 111 | await Global.Fetch(Global.format(URL.USER_PROMPT_DELETE, promptId), "post"); 112 | } 113 | 114 | function modalOkPressed() { 115 | let prompt = {} as UserPrompt; 116 | prompt.promptId = modalId; 117 | prompt.text = modalText; 118 | if (modalMode === ModalModeE.ADD) { 119 | addPrompt(prompt); 120 | } 121 | else if (modalMode === ModalModeE.EDIT) { 122 | updatePrompt(prompt); 123 | } 124 | } 125 | 126 | return ( 127 | 128 | 132 | 133 | 134 | {modalTitle} 135 | setModalText(text)}> 140 | 141 | 142 | 147 | 153 | 154 | 155 | 156 | 157 | 158 | {[...prompts].map(([id, prompt]) => ( 159 | 160 | {i18n.t('profile.prompts.' + id)} 161 | {prompt.text} 162 | 163 | openModal(ModalModeE.DELETE, prompt)} 167 | /> 168 | openModal(ModalModeE.EDIT, prompt)} 173 | /> 174 | 175 | 176 | ))} 177 | { 178 | prompts.size < maxPrompts && 179 | {i18n.t('profile.prompts.add')} 180 | } 181 | { 182 | prompts.size < maxPrompts && promptIdArray.filter(index => !prompts.has(index)).map(index => ( 183 | { 184 | openModal(ModalModeE.ADD, { promptId: index, text: "" }); 185 | }}> 186 | 187 | {i18n.t('profile.prompts.' + (index)) + '…'} 188 | 189 | 190 | )) 191 | } 192 | 193 | 194 | 195 | ); 196 | }; 197 | 198 | export default Prompts; 199 | -------------------------------------------------------------------------------- /screens/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Search, Likes, Messages, YourProfile, Donate } from "../screens"; 3 | import * as Global from "../Global"; 4 | import * as URL from "../URL"; 5 | import * as I18N from "../i18n"; 6 | import { MaterialCommunityIcons } from "@expo/vector-icons"; 7 | import { NAVIGATION_BAR_HEIGHT } from "../assets/styles"; 8 | import { useWindowDimensions } from "react-native"; 9 | import { RootStackParamList, YourProfileResource, UserDto } from "../myTypes"; 10 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 11 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 12 | import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; 13 | import { useTheme } from "react-native-paper"; 14 | 15 | const Tab = createBottomTabNavigator(); 16 | const i18n = I18N.getI18n() 17 | const ICON_SIZE = 26; 18 | const SECOND_MS = 1000; 19 | const POLL_ALERT = 15 * SECOND_MS; 20 | const POLL_MESSAGE = 15 * SECOND_MS; 21 | const MOBILE_WIDTH = 768; //from react navigation 22 | 23 | const YourProfileScreen: React.FC = (props) => ; 24 | const MessagesScreen: React.FC = (props) => ; 25 | const SearchScreen: React.FC = (props) => ; 26 | const LikesScreen: React.FC = (props) => ; 27 | const DonateScreen: React.FC = (props) => ; 28 | 29 | type Props = BottomTabScreenProps 30 | const Main = ({ route, navigation }: Props) => { 31 | 32 | const { width } = useWindowDimensions(); 33 | const insets = useSafeAreaInsets() 34 | const { colors } = useTheme(); 35 | 36 | let messageUpdateInterval: NodeJS.Timeout | undefined; 37 | let alertUpdateInterval: NodeJS.Timeout | undefined; 38 | let langIso: string | undefined; 39 | 40 | const [newAlert, setNewAlert] = React.useState(false); 41 | const [newMessage, setHasNewMessage] = React.useState(false); 42 | const [incompleteProfile, setIncompleteProfile] = React.useState(false); 43 | 44 | async function updateNewAlert() { 45 | let url; 46 | if (!langIso) { 47 | langIso = i18n.locale.slice(0, 2); 48 | url = Global.format(URL.USER_STATUS_ALERT_LANG, langIso); 49 | } else { 50 | url = URL.USER_STATUS_ALERT; 51 | } 52 | let response = await Global.Fetch(url); 53 | let data: boolean = response.data; 54 | setNewAlert(data); 55 | } 56 | 57 | async function checkProfileIncomplete() { 58 | let response = await Global.Fetch(URL.API_RESOURCE_YOUR_PROFILE); 59 | let data: YourProfileResource = response.data; 60 | let user: UserDto = data.user; 61 | 62 | if (user.interests.length === 0) { 63 | setIncompleteProfile(true); 64 | } else if (user.images.length == 0) { 65 | setIncompleteProfile(true); 66 | } else if (user.prompts.length == 0) { 67 | setIncompleteProfile(true); 68 | } 69 | } 70 | 71 | async function updateNewMessage() { 72 | let response = await Global.Fetch(URL.USER_STATUS_MESSAGE); 73 | let data: boolean = response.data; 74 | setHasNewMessage(data); 75 | } 76 | 77 | React.useEffect(() => { 78 | messageUpdateInterval = setInterval(async () => { 79 | updateNewAlert(); 80 | }, POLL_MESSAGE); 81 | 82 | alertUpdateInterval = setInterval(async () => { 83 | updateNewMessage(); 84 | }, POLL_ALERT); 85 | 86 | updateNewAlert(); 87 | updateNewMessage(); 88 | checkProfileIncomplete(); 89 | 90 | Global.SetStorage(Global.STORAGE_SCREEN, Global.SCREEN_SEARCH); 91 | }, []); 92 | 93 | React.useEffect(() => { 94 | const unsubscribe = navigation.addListener('beforeRemove', () => { 95 | if (messageUpdateInterval) { 96 | clearInterval(messageUpdateInterval); 97 | } 98 | if (alertUpdateInterval) { 99 | clearInterval(alertUpdateInterval); 100 | } 101 | }); 102 | return unsubscribe; 103 | }, [navigation]); 104 | 105 | function saveScreen(target: string | undefined) { 106 | if (target) { 107 | let targetSplitArr = target.split("-"); 108 | let screen = targetSplitArr[0]; 109 | switch (screen) { 110 | case Global.SCREEN_YOURPROFILE: Global.SetStorage(Global.STORAGE_SCREEN, Global.SCREEN_YOURPROFILE); break; 111 | case Global.SCREEN_CHAT: Global.SetStorage(Global.STORAGE_SCREEN, Global.SCREEN_CHAT); break; 112 | case Global.SCREEN_SEARCH: Global.SetStorage(Global.STORAGE_SCREEN, Global.SCREEN_SEARCH); break; 113 | case Global.SCREEN_LIKES: Global.SetStorage(Global.STORAGE_SCREEN, Global.SCREEN_LIKES); break; 114 | case Global.SCREEN_DONATE: Global.SetStorage(Global.STORAGE_SCREEN, Global.SCREEN_DONATE); break; 115 | } 116 | } 117 | } 118 | 119 | return ( 120 | = MOBILE_WIDTH ? 0 : 12, //react navigation workaround, no way to center icons while having a custom height 126 | }, 127 | tabBarBadgeStyle: { 128 | backgroundColor: 'red', 129 | minWidth: 8, 130 | height: 8, 131 | }, 132 | headerShown: false, 133 | tabBarActiveTintColor: colors.primary, 134 | }}> 135 | { 140 | saveScreen(e.target); 141 | }, 142 | }} 143 | options={{ 144 | tabBarBadge: incompleteProfile ? "" : undefined, 145 | tabBarLabel: i18n.t('navigation.profile'), 146 | tabBarIcon: ({ color }) => ( 147 | 148 | ), 149 | }} 150 | /> 151 | { 156 | saveScreen(e.target); 157 | }, 158 | }} 159 | options={{ 160 | tabBarBadge: newMessage ? "" : undefined, 161 | tabBarLabel: i18n.t('navigation.chat'), 162 | tabBarIcon: ({ color }) => ( 163 | 164 | ), 165 | }} 166 | /> 167 | { 172 | saveScreen(e.target); 173 | }, 174 | }} 175 | options={{ 176 | tabBarLabel: i18n.t('navigation.search'), 177 | tabBarIcon: ({ color }) => ( 178 | 179 | ), 180 | }} 181 | /> 182 | { 187 | saveScreen(e.target); 188 | }, 189 | }} 190 | options={{ 191 | tabBarBadge: newAlert ? "" : undefined, 192 | tabBarLabel: i18n.t('navigation.likes'), 193 | tabBarIcon: ({ color }) => ( 194 | 195 | ), 196 | }} 197 | /> 198 | { 203 | saveScreen(e.target); 204 | }, 205 | }} 206 | options={{ 207 | tabBarLabel: i18n.t('navigation.donate'), 208 | tabBarIcon: ({ color }) => ( 209 | 210 | ), 211 | }} 212 | /> 213 | 214 | ) 215 | }; 216 | 217 | export default Main; -------------------------------------------------------------------------------- /screens/Likes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | TouchableOpacity, 5 | FlatList, 6 | RefreshControl, 7 | Pressable, 8 | useWindowDimensions, 9 | Image 10 | } from "react-native"; 11 | 12 | import { ActivityIndicator, IconButton, Menu, Modal, Portal, Text, useTheme } from "react-native-paper"; 13 | import { CardItemLikes } from "../components"; 14 | import styles, { NAVIGATION_BAR_HEIGHT, STATUS_BAR_HEIGHT, WIDESCREEN_HORIZONTAL_MAX } from "../assets/styles"; 15 | import * as I18N from "../i18n"; 16 | import * as Global from "../Global"; 17 | import * as URL from "../URL"; 18 | import { AlertsResource, UserDto, UnitsEnum, UserUsersResource, LikeResultT, RootStackParamList } from "../myTypes"; 19 | import LikesEmpty from "../assets/images/likes-empty.svg"; 20 | import { MaterialCommunityIcons } from "@expo/vector-icons"; 21 | import VerticalView from "../components/VerticalView"; 22 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 23 | 24 | type Props = BottomTabScreenProps 25 | 26 | const Likes = ({ navigation }: Props) => { 27 | 28 | const { colors } = useTheme(); 29 | const i18n = I18N.getI18n() 30 | 31 | enum FILTER { 32 | RECEIVED_LIKES, 33 | GIVEN_LIKES, 34 | HIDDEN, 35 | BLOCKED 36 | } 37 | 38 | const [loaded, setLoaded] = React.useState(false); 39 | const [refreshing] = React.useState(false); // todo: setRefreshing 40 | const [user, setUser] = React.useState(); 41 | const [results, setResults] = React.useState(Array); 42 | const [menuFilterVisible, setMenuFilterVisible] = React.useState(false); 43 | const [filter, setFilter] = React.useState(FILTER.RECEIVED_LIKES); 44 | const [loading, setLoading] = React.useState(false); 45 | const [visible, setVisible] = React.useState(false); 46 | const [likeResult, setLikeResult] = React.useState(); 47 | const { height, width } = useWindowDimensions(); 48 | 49 | const svgHeight = 150; 50 | const svgWidth = 200; 51 | const topBarHeight = 62; 52 | 53 | const containerStyle = { backgroundColor: colors.surface, padding: 24, marginHorizontal: calcMarginModal(), borderRadius: 8 }; 54 | 55 | function calcMarginModal() { 56 | return width < WIDESCREEN_HORIZONTAL_MAX + 12 ? 12 : width / 5 + 12; 57 | } 58 | const showModal = () => setVisible(true); 59 | const hideModal = () => setVisible(false); 60 | 61 | async function load() { 62 | setLoading(true); 63 | setMenuFilterVisible(false); 64 | 65 | let url; 66 | switch (filter) { 67 | case FILTER.RECEIVED_LIKES: url = URL.API_RESOURCE_ALERTS; break; 68 | case FILTER.GIVEN_LIKES: url = URL.API_RESOURCE_USER_LIKED; break; 69 | case FILTER.HIDDEN: url = URL.API_RESOURCE_USER_HIDDEN; break; 70 | case FILTER.BLOCKED: url = URL.API_RESOURCE_USER_BLOCKED; break; 71 | } 72 | if (url) { 73 | await Global.Fetch(url).then( 74 | (response) => { 75 | if (filter === FILTER.RECEIVED_LIKES) { 76 | let data: AlertsResource = response.data; 77 | setUser(data.user); 78 | let res = data.notifications.map(item => { 79 | let t = {} as LikeResultT; 80 | t.message = item.message; 81 | t.user = item.userFromDto; 82 | return t; 83 | }); 84 | setResults(res); 85 | } else { 86 | let data: UserUsersResource = response.data; 87 | setUser(data.user); 88 | let res = data.users.map(item => { 89 | let t = {} as LikeResultT; 90 | t.user = item; 91 | return t; 92 | }); 93 | setResults(res); 94 | } 95 | } 96 | ); 97 | setLoaded(true); 98 | setLoading(false); 99 | } 100 | } 101 | 102 | React.useEffect(() => { 103 | const unsubscribe = navigation.addListener('focus', () => { 104 | if (filter !== FILTER.RECEIVED_LIKES) { 105 | load(); 106 | } 107 | setFilter(FILTER.RECEIVED_LIKES); 108 | }); 109 | return unsubscribe; 110 | }, [navigation]); 111 | 112 | React.useEffect(() => { 113 | setVisible(false); 114 | load(); 115 | }, [filter]); 116 | 117 | function onMessagePressed(result: LikeResultT) { 118 | setLikeResult(result); 119 | showModal(); 120 | } 121 | 122 | return ( 123 | 124 | {loading && 125 | 126 | 127 | 128 | } 129 | 130 | 131 | {filter === FILTER.RECEIVED_LIKES && {i18n.t('likes.received-likes')}} 132 | {filter === FILTER.GIVEN_LIKES && {i18n.t('likes.given-likes')}} 133 | {filter === FILTER.HIDDEN && {i18n.t('likes.hidden')}} 134 | {filter === FILTER.BLOCKED && {i18n.t('likes.blocked')}} 135 | 136 | setMenuFilterVisible(false)} 139 | anchor={ setMenuFilterVisible(true)}>}> 140 | {filter !== FILTER.RECEIVED_LIKES && setFilter(FILTER.RECEIVED_LIKES)} title={i18n.t('likes.received-likes')} />} 141 | {filter !== FILTER.GIVEN_LIKES && setFilter(FILTER.GIVEN_LIKES)} title={i18n.t('likes.given-likes')} />} 142 | {filter !== FILTER.HIDDEN && setFilter(FILTER.HIDDEN)} title={i18n.t('likes.hidden')} />} 143 | {filter !== FILTER.BLOCKED && setFilter(FILTER.BLOCKED)} title={i18n.t('likes.blocked')} />} 144 | 145 | 146 | 147 | 148 | } 151 | columnWrapperStyle={{ flex: 1, justifyContent: "space-around" }} 152 | numColumns={2} 153 | data={results} 154 | keyExtractor={(item, index) => index.toString()} 155 | renderItem={({ item }) => ( 156 | 157 | 163 | 164 | )} 165 | /> 166 | {results && results.length === 0 && loaded && filter === FILTER.RECEIVED_LIKES && 167 | 168 | 169 | {i18n.t('likes-empty.title')} 170 | {i18n.t('likes-empty.subtitle')} 171 | 172 | } 173 | 174 | 175 | 176 | 177 | 183 | 184 | 185 | 186 | {likeResult?.user.firstName + ", " + likeResult?.user.age} 187 | 188 | 189 | {likeResult?.message} 190 | 191 | 192 | 193 | 194 | ) 195 | }; 196 | 197 | export default Likes; 198 | -------------------------------------------------------------------------------- /Global.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react-native'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import * as SecureStore from 'expo-secure-store'; 4 | import AsyncStorage from '@react-native-async-storage/async-storage'; 5 | import * as URL from "./URL"; 6 | import { createNavigationContainerRef, CommonActions } from '@react-navigation/native'; 7 | import { ConversationDto, RootStackParamList, UserDto } from "./myTypes"; 8 | import Toast from 'react-native-toast-message'; 9 | import * as ImagePicker from 'expo-image-picker'; 10 | import * as ImageManipulator from 'expo-image-manipulator'; 11 | import { Platform } from 'react-native'; 12 | import mime from "mime"; 13 | import FormData from "form-data"; 14 | import { Buffer } from "buffer"; 15 | import { cloneDeep } from 'lodash'; 16 | 17 | export const FLAG_ENABLE_DONATION = true; 18 | 19 | export const navigationRef = createNavigationContainerRef() 20 | export const INDEX_LOGIN = "0" 21 | export const INDEX_REGISTER = "1" 22 | export const INDEX_ONBOARDING = "2" 23 | export const INDEX_MAIN = "3" 24 | 25 | export const STORAGE_FIRSTNAME = "firstName"; 26 | export const STORAGE_PAGE = "page"; 27 | export const STORAGE_SCREEN = "screen"; 28 | export const STORAGE_YOUR_PROFILE = "your-profile" 29 | export const STORAGE_YOUR_CHAT = "chat" 30 | export const STORAGE_YOUR_CHAT_DETAIL = "chat/%s" 31 | export const STORAGE_LIKES = "likes" 32 | export const STORAGE_DONATE = "donate" 33 | export const STORAGE_LATITUDE = "latitude" 34 | export const STORAGE_LONGITUDE = "longitude" 35 | export const STORAGE_RELOAD_SEARCH = "reloadSearch"; 36 | export const STORAGE_LOGIN_DATE = "loginDate"; 37 | export const STORAGE_SEARCH_LIKE_TOOLTIP = "search.like-tooltip"; 38 | export const STORAGE_SEARCH_REMOVE_TOP = "search.remove-top"; 39 | 40 | export const STORAGE_SETTINGS_UNIT = "settings.unit" 41 | export const STORAGE_SETTINGS_COLOR_PRIMARY = "settings.color.primary" 42 | export const STORAGE_SETTINGS_COLOR_SECONDARY = "settings.color.secondary" 43 | 44 | export const STORAGE_ADV_SEARCH_GPSTIMEOPUT = "adv-search.gps-timeout"; 45 | export const STORAGE_ADV_SEARCH_HIDE_THRESHOLD = "adv-search.hide-threshold"; 46 | export const STORAGE_ADV_SEARCH_PARAMS = "adv-search.params"; 47 | 48 | export const STORAGE_TRUE = "true"; 49 | export const STORAGE_FALSE = "false"; 50 | 51 | export const SCREEN_YOURPROFILE = "YourProfile" 52 | export const SCREEN_CHAT = "Chat" 53 | export const SCREEN_SEARCH = "Search" 54 | export const SCREEN_LIKES = "Likes" 55 | export const SCREEN_DONATE = "Donate" 56 | 57 | export const SCREEN_PROFILE_PICTURES = "Profile.Pictures" 58 | export const SCREEN_PROFILE_PROFILESETTINGS = "Profile.ProfileSettings" 59 | export const SCREEN_PROFILE_SEARCHSETTINGS = "Profile.SearchSettings" 60 | export const SCREEN_PROFILE_SEARCHPARAMETERS = "Profile.SearchParameters" 61 | export const SCREEN_PROFILE_SETTINGS = "Profile.Settings" 62 | export const SCREEN_PROFILE_ADVANCED_SETTINGS = "Profile.AdvancedSettings" 63 | 64 | export const DEFAULT_COLOR_PRIMARY = '#EC407A'; 65 | export const DEFAULT_COLOR_SECONDARY = '#28C4ED'; 66 | 67 | export const EMPTY_STRING = "..."; 68 | 69 | export const MAX_INTERESTS = 10; 70 | export const MAX_MESSAGE_LENGTH = 255; 71 | export const MAX_DESCRIPTION_LENGTH = 255; 72 | export const DEFAULT_GPS_TIMEOUT = 6000; 73 | export const DEFAULT_HIDE_THRESHOLD = 3; 74 | export const DEFAULT_DISTANCE = 120; 75 | export const MAX_DISTANCE = 160; 76 | 77 | const IMG_SIZE_MAX = 600; 78 | 79 | export async function Fetch(url: string = "", method: string = "get", data: any = undefined, 80 | contentType: string = "application/json"): Promise> { 81 | try { 82 | let res = await axios({ 83 | withCredentials: true, 84 | method: method, 85 | url: url, 86 | headers: { 87 | 'Content-Type': contentType 88 | }, 89 | data: data, 90 | }); 91 | if (res.request.responseURL === URL.AUTH_LOGIN) { 92 | let page = await GetStorage(STORAGE_PAGE); 93 | if (page !== INDEX_LOGIN) { 94 | SetStorage(STORAGE_PAGE, INDEX_LOGIN); 95 | navigate("Login"); 96 | throw new Error("Not authenticated") 97 | } 98 | } 99 | return res; 100 | } catch (e) { 101 | console.error(e) 102 | throw e; 103 | } 104 | } 105 | 106 | export function nagivateProfile(user?: UserDto, uuid?: string) { 107 | navigate("Profile", false, { 108 | user: user, 109 | uuid: uuid 110 | }); 111 | } 112 | 113 | export function nagivateChatDetails(conversation: ConversationDto) { 114 | navigate("MessageDetail", false, { 115 | conversation: conversation 116 | }); 117 | } 118 | 119 | export function navigate(name: string, reset: boolean = false, params?: any) { 120 | if (navigationRef.isReady()) { 121 | if (!reset) { 122 | navigationRef.navigate(name, params); 123 | } else { 124 | navigationRef.dispatch( 125 | CommonActions.reset({ 126 | index: 1, 127 | routes: [{ name: name }], 128 | }) 129 | ); 130 | } 131 | } 132 | } 133 | 134 | export async function GetStorage(key: string): Promise { 135 | if (React.Platform.OS === 'web') { 136 | return await AsyncStorage.getItem(key); 137 | } else { 138 | return await SecureStore.getItemAsync(key); 139 | } 140 | } 141 | 142 | export async function SetStorage(key: string, value: string) { 143 | if (React.Platform.OS === 'web') { 144 | await AsyncStorage.setItem(key, value); 145 | } else { 146 | await SecureStore.setItemAsync(key, value); 147 | } 148 | } 149 | 150 | export function loadPage(page: string = INDEX_REGISTER) { 151 | if (INDEX_ONBOARDING === page) { 152 | navigate("Onboarding"); 153 | } else if (INDEX_MAIN === page) { 154 | navigate("Main", true); 155 | } else if (INDEX_REGISTER === page) { 156 | navigate("Register"); 157 | } 158 | } 159 | 160 | export function ShowToast(text: string) { 161 | if (React.Platform.OS === 'android') { 162 | React.ToastAndroid.show(text, React.ToastAndroid.LONG); 163 | } else { 164 | Toast.show({ 165 | text1: text, 166 | visibilityTime: 3000, 167 | position: 'top' 168 | }); 169 | } 170 | } 171 | 172 | export function isEmailValid(text: string) { 173 | let reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 174 | return reg.test(text); 175 | } 176 | 177 | export function isPasswordSecure(password: string): boolean { 178 | const minPasswordLength = 7; 179 | if (password.length < minPasswordLength) { 180 | return false; 181 | } 182 | return password.match(/[a-z]/i) != null && password.match(/[0-9]+/) != null; 183 | } 184 | 185 | export function calcAge(dob: Date | undefined): number { 186 | if (!dob) { 187 | return Number.MIN_VALUE; 188 | } 189 | let timeDiff = Math.abs(Date.now() - dob.getTime()); 190 | let age = Math.floor((timeDiff / (1000 * 3600 * 24)) / 365.25); 191 | return age; 192 | } 193 | 194 | export async function pickImage(): Promise { 195 | let result = await ImagePicker.launchImageLibraryAsync({ 196 | mediaTypes: 'images', 197 | allowsEditing: true, 198 | aspect: [1, 1], 199 | quality: 1, 200 | base64: true, 201 | exif: true, 202 | }); 203 | if (!result.canceled) { 204 | let format = ImageManipulator.SaveFormat.JPEG; 205 | const saveOptions: ImageManipulator.SaveOptions = { compress: 0.8, format: format, base64: true } 206 | const resizedImageData = await ImageManipulator.manipulateAsync( 207 | result.assets[0].uri, 208 | [{ resize: { width: IMG_SIZE_MAX, height: Platform.OS === 'android' ? IMG_SIZE_MAX : undefined } }], 209 | saveOptions 210 | ); 211 | if (Platform.OS !== 'web') { 212 | return Platform.select({ ios: resizedImageData.uri.replace('file://', ''), android: resizedImageData.uri }) 213 | } else { 214 | return resizedImageData.base64; 215 | } 216 | } else { 217 | return null; 218 | } 219 | }; 220 | 221 | export function buildFormData(imageData: string): FormData { 222 | 223 | const mimeType = mime.getType(imageData); 224 | const bodyFormData = new FormData(); 225 | if (Platform.OS !== "web") { 226 | bodyFormData.append('file', { 227 | name: imageData.split("/").pop(), 228 | type: mimeType, 229 | uri: imageData, 230 | }); 231 | } else { 232 | const buffer = Buffer.from(imageData, "base64"); 233 | const blob = new Blob([buffer]); 234 | bodyFormData.append('file', blob); 235 | } 236 | bodyFormData.append('mime', mimeType); 237 | return bodyFormData; 238 | }; 239 | 240 | export function shuffleArray(array: any[]): any[] { 241 | const copy: any[] = cloneDeep(array); 242 | for (let i = copy.length - 1; i > 0; i--) { 243 | let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i 244 | [copy[i], copy[j]] = [copy[j], copy[i]]; 245 | } 246 | return copy; 247 | } 248 | 249 | export const format = (str: string, ...args: any[]) => args.reduce((s, v) => s.replace('%s', v), str); -------------------------------------------------------------------------------- /screens/profile/SearchSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | useWindowDimensions 5 | } from "react-native"; 6 | import { ActivityIndicator, Checkbox, Divider, Text, useTheme } from "react-native-paper"; 7 | import { YourProfileResource, GenderEnum, UserIntention, Gender, IntentionE, SearchParams, IntentionNameMap, GenderNameMap, RootStackParamList } from "../../myTypes"; 8 | import * as I18N from "../../i18n"; 9 | import * as Global from "../../Global"; 10 | import * as URL from "../../URL"; 11 | import SelectModal from "../../components/SelectModal"; 12 | import AgeRangeSliderModal from "../../components/AgeRangeSliderModal"; 13 | import VerticalView from "../../components/VerticalView"; 14 | import { useHeaderHeight } from '@react-navigation/elements'; 15 | import Slider from "@react-native-community/slider"; 16 | import { GRAY } from "../../assets/styles"; 17 | import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; 18 | 19 | const i18n = I18N.getI18n() 20 | const MIN_AGE = 18; 21 | const MAX_AGE = 100; 22 | 23 | type Props = BottomTabScreenProps 24 | 25 | const SearchSettings = ({ route }: Props) => { 26 | 27 | //var data: YourProfileResource = route.params.data; 28 | const { colors } = useTheme(); 29 | const { height, width } = useWindowDimensions(); 30 | const headerHeight = useHeaderHeight(); 31 | 32 | const [data, setData] = React.useState(route.params.data); 33 | const [isLegal, setIsLegal] = React.useState(false); 34 | const [intention, setIntention] = React.useState(IntentionE.MEET); 35 | const [showIntention, setShowIntention] = React.useState(false); 36 | const [minAge, setMinAge] = React.useState(MIN_AGE) 37 | const [maxAge, setMaxAge] = React.useState(MAX_AGE) 38 | const [preferredGenders, setPreferredGenders] = React.useState(Array); 39 | const [settingsIgnoreIntention, setSettingsIgnoreIntention] = React.useState(false); 40 | const [loading, setLoading] = React.useState(false); 41 | const [changed, setChanged] = React.useState(false); 42 | 43 | const minDistance = 1; 44 | const [maxDistance] = React.useState(Global.MAX_DISTANCE); // todo: setMaxDistance 45 | const [distance, setDistance] = React.useState(Global.DEFAULT_DISTANCE); 46 | const [distanceText, setDistanceText] = React.useState(distance); 47 | const [distanceUnit] = React.useState("km"); // todo: setDistanceUnit 48 | const [params, setParams] = React.useState(); 49 | const [showOutsideParams, setShowOutsideParams] = React.useState(true); 50 | 51 | async function load() { 52 | setLoading(true); 53 | let response = await Global.Fetch(URL.API_RESOURCE_YOUR_PROFILE); 54 | let data: YourProfileResource = response.data; 55 | setData(data) 56 | loadUser(data); 57 | } 58 | 59 | async function loadUser(data: YourProfileResource) { 60 | setLoading(true); 61 | setShowIntention(data.showIntention); 62 | setIsLegal(data.user.age >= MIN_AGE); 63 | setMinAge(data.user.preferedMinAge); 64 | setMaxAge(data.user.preferedMaxAge); 65 | setIntention(data.user.intention.id); 66 | setPreferredGenders(data.user.preferedGenders.map(item => item.id)); 67 | setSettingsIgnoreIntention(data["settings.ignoreIntention"]); 68 | let paramsStorage = await Global.GetStorage(Global.STORAGE_ADV_SEARCH_PARAMS); 69 | setParams(paramsStorage ? JSON.parse(paramsStorage) : {}); 70 | setLoading(false); 71 | } 72 | 73 | async function onDistanceChanged(value: number) { 74 | let params: SearchParams = await getStoredParams(); 75 | params.distance = value; 76 | setParams(params); 77 | } 78 | 79 | async function toggleShowOutsideParams() { 80 | let newState = !showOutsideParams; 81 | setShowOutsideParams(newState); 82 | let params: SearchParams = await getStoredParams(); 83 | params.showOutsideParameters = newState; 84 | setParams(params); 85 | } 86 | 87 | async function getStoredParams(): Promise { 88 | let paramsStorage = await Global.GetStorage(Global.STORAGE_ADV_SEARCH_PARAMS); 89 | let params: SearchParams = paramsStorage ? JSON.parse(paramsStorage) : {}; 90 | return params; 91 | } 92 | 93 | React.useEffect(() => { 94 | Global.SetStorage(Global.STORAGE_RELOAD_SEARCH, Global.STORAGE_FALSE); 95 | if (data) { 96 | loadUser(data); 97 | } else { 98 | load(); 99 | } 100 | }, []); 101 | 102 | React.useEffect(() => { 103 | if (changed) { 104 | Global.SetStorage(Global.STORAGE_RELOAD_SEARCH, Global.STORAGE_TRUE); 105 | setChanged(false) 106 | } 107 | }, [changed]); 108 | 109 | React.useEffect(() => { 110 | //TODO 111 | //let isIS = data.user.units == UnitsEnum.SI; 112 | let saveParam = false; 113 | if(params?.distance) { 114 | setDistance(params.distance); 115 | saveParam = true; 116 | } 117 | if (params?.showOutsideParameters !== undefined) { 118 | setShowOutsideParams(params.showOutsideParameters); 119 | saveParam = true; 120 | } 121 | if(saveParam) { 122 | saveParams() 123 | } 124 | }, [params]); 125 | 126 | async function saveParams() { 127 | if (params) { 128 | await Global.SetStorage(Global.STORAGE_ADV_SEARCH_PARAMS, JSON.stringify(params)); 129 | setChanged(true); 130 | } 131 | } 132 | 133 | async function updateIntention(num: number) { 134 | await Global.Fetch(Global.format(URL.USER_UPDATE_INTENTION, String(num)), 'post'); 135 | Global.ShowToast(i18n.t('profile.intention-toast')); 136 | setIntention(num); 137 | setShowIntention(false); 138 | 139 | let intention: UserIntention = { id: num, text: "" }; 140 | data.user.intention = intention; 141 | setChanged(true); 142 | } 143 | 144 | async function updateGenders(genderId: number, state: boolean) { 145 | await Global.Fetch(Global.format(URL.USER_UPDATE_PREFERED_GENDER, genderId, state ? "1" : "0"), 'post'); 146 | if (state) { 147 | let gender: Gender = { 148 | id: genderId, 149 | text: "" 150 | }; 151 | data.user.preferedGenders.push(gender); 152 | } else { 153 | data.user.preferedGenders.forEach((item, index) => { 154 | if (item.id === genderId) data.user.preferedGenders.splice(index, 1); 155 | }); 156 | } 157 | setChanged(true); 158 | } 159 | 160 | async function updateMinAge(num: number) { 161 | await Global.Fetch(Global.format(URL.USER_UPDATE_MIN_AGE, String(num)), 'post'); 162 | setMinAge(num); 163 | data.user.preferedMinAge = num; 164 | setChanged(true); 165 | } 166 | 167 | async function updateMaxAge(num: number) { 168 | await Global.Fetch(Global.format(URL.USER_UPDATE_MAX_AGE, String(num)), 'post'); 169 | setMaxAge(num); 170 | data.user.preferedMaxAge = num; 171 | setChanged(true); 172 | } 173 | 174 | return ( 175 | 176 | {loading && 177 | 178 | 179 | 180 | } 181 | 182 | 183 | 184 | 185 | 186 | {!settingsIgnoreIntention && 187 | 188 | 197 | 198 | } 199 | 200 | 201 | 210 | 211 | 212 | {isLegal && 213 | 214 | 216 | 217 | } 218 | 219 | 220 | 221 | 222 | {i18n.t('profile.search.settings.distance') + ": " + distanceText + " " + distanceUnit} 223 | 224 | { 234 | setDistanceText(value); 235 | }} 236 | onSlidingComplete={(value: number) => { 237 | onDistanceChanged(value); 238 | }} 239 | /> 240 | 241 | 242 | 243 | 244 | 246 | 247 | 248 | 249 | 250 | 251 | ); 252 | }; 253 | 254 | export default SearchSettings; 255 | -------------------------------------------------------------------------------- /assets/onboarding/description.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 35 | 40 | 45 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 87 | 91 | 96 | 100 | 105 | 110 | 115 | 121 | 127 | 133 | 138 | 143 | 148 | 149 | --------------------------------------------------------------------------------