Compare commits

..

16 commits
v1.9.1 ... main

38 changed files with 1543 additions and 271 deletions

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
liberapay: alerte-secours
github: alerte-secours
buy_me_a_coffee: alertesecours

View file

@ -2,6 +2,37 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.10.1](https://github.com/alerte-secours/as-app/compare/v1.10.0...v1.10.1) (2025-06-01)
### Bug Fixes
* remove deprecated ([a2f476f](https://github.com/alerte-secours/as-app/commit/a2f476f8ee9670461dc7babc7f534c43ba4708e9))
## [1.10.0](https://github.com/alerte-secours/as-app/compare/v1.9.2...v1.10.0) (2025-06-01)
### Features
* contribute ([3e70ff2](https://github.com/alerte-secours/as-app/commit/3e70ff23c9361a5d331e15160adbb44a8bf773ac))
* **follow-location:** init ([ca3a2c8](https://github.com/alerte-secours/as-app/commit/ca3a2c8fcce2aaa613817bde047f143915ef8fe0))
* **follow-location:** map bubble ([f3cca81](https://github.com/alerte-secours/as-app/commit/f3cca8182ca52a89f7f6d0785821185db277bbf0))
* **follow-location:** map wip ([ce11db9](https://github.com/alerte-secours/as-app/commit/ce11db9863a7aaa14b28e104645d8ce0b23a235b))
* **follow-location:** overview initial position ([30aeb2a](https://github.com/alerte-secours/as-app/commit/30aeb2a0e41196719977900e08b12d8ae813a69a))
* **follow-location:** wip ([c30c0b0](https://github.com/alerte-secours/as-app/commit/c30c0b0482d946b6b4d6a2421416a59f9d57bfeb))
### Bug Fixes
* call emergency by default on yellow ([991b65d](https://github.com/alerte-secours/as-app/commit/991b65d990b909fb02f9941fedf84637d0a8b71e))
## [1.9.2](https://github.com/alerte-secours/as-app/compare/v1.9.1...v1.9.2) (2025-05-16)
### Bug Fixes
* **ios-reported-bug:** app only displayed the splash screen after enabling access to location ([2e35c41](https://github.com/alerte-secours/as-app/commit/2e35c41e0f3e968df6dc07e7656cf50509557a7c))
## [1.9.1](https://github.com/alerte-secours/as-app/compare/v1.9.0...v1.9.1) (2025-05-14)

226
DEVELOPER.md Normal file
View file

@ -0,0 +1,226 @@
# Alerte Secours Mobile App - Developer Documentation
This document contains technical information for developers working on the Alerte Secours mobile application.
## Table of Contents
- [Project Overview](#project-overview)
- [Technical Stack](#technical-stack)
- [Development Quick Start](#development-quick-start)
- [Installation](#installation)
- [Android](#android)
- [iOS](#ios)
- [Project Structure](#project-structure)
- [Troubleshooting](#troubleshooting)
## Project Overview
Alerte Secours is a mobile application built with React Native that handles alerts and emergency-related functionality. The app supports both iOS and Android platforms and includes features such as:
- Alert creation and management with real-time updates
- Location-based features with mapping integration
- Chat/Messaging system with alert-specific chat rooms
- Authentication via SMS verification
- Deep linking for alert sharing
- Push notifications
## Technical Stack
- React Native
- Expo framework
- GraphQL with Hasura
- Apollo Client for frontend
- Federated remote schemas
- Real-time subscriptions support
- Firebase Cloud Messaging (FCM) for push notifications only
- Sentry for error tracking
- MapLibre for mapping functionality
- Zustand for state management
- i18next for internationalization
- React Navigation for navigation
- Expo Updates for OTA updates
- Background Geolocation for location tracking
- Lottie for animations
- React Hook Form for form handling
- Axios for HTTP requests
- Yarn Berry as package manager
- ESLint and Prettier for code quality
- Fastlane for deployment automation
## Development Quick Start
### Prerequisites
- Node.js (version specified in `.node-version`)
- Yarn package manager
- Android Studio (for Android development)
- Xcode (for iOS development)
- Physical device or emulator/simulator
### Environment Setup
1. Clone the repository
2. Install dependencies:
```bash
yarn
```
3. Copy the staging environment file:
```bash
cp .env.staging.example .env.staging
```
4. Start the development server with staging environment:
```bash
yarn start:staging
```
### Running on Devices
#### Android
```bash
yarn android:staging
```
#### iOS
```bash
yarn ios:staging
```
### Staging URLs
The staging environment uses the following URLs:
- GraphQL API: `https://hasura-staging.alertesecours.fr/v1/graphql`
- WebSocket: `wss://hasura-staging.alertesecours.fr/v1/graphql`
- Files API: `https://files-staging.alertesecours.fr/api/v1/oas`
- Minio: `https://minio-staging.alertesecours.fr`
- Geolocation Sync: `https://api-staging.alertesecours.fr/api/v1/oas/geoloc/sync`
## Installation
### Android
#### Using the Yarn Script
The easiest way to install the app is to use the provided yarn script:
```bash
# Set the device ID (emulator or physical device)
export DEVICE=emulator-5554
# Run the installation script
yarn install:android
```
This script (`install-android.sh`) handles the entire installation process, including building APKs with signing, extracting them, and installing on the device.
#### Manual Installation
If you need to install the app manually, you can examine the `install-android.sh` script in the project root to see the detailed steps involved.
### iOS
#### Authentication Key Setup
1. Go to https://appstoreconnect.apple.com/access/integrations/api
2. Click the "+" button to generate a new API key
3. Give it a name (e.g., "AlerteSecours Build Key")
4. Download the .p8 file when prompted
5. Store the .p8 file in a secure location
6. Note down the Key ID and Issuer ID shown on the website
7. Set up environment variables:
```sh
export ASC_API_KEY_ID="YOUR_KEY_ID"
export ASC_API_ISSUER_ID="YOUR_ISSUER_ID"
export ASC_API_KEY_PATH="/path/to/your/AuthKey.p8"
```
#### Building and Running
To build and run the iOS app:
```bash
# Run in development mode with staging environment
yarn ios:staging
# Build for production (uses scripts/ios-archive.sh and scripts/ios-export.sh)
yarn bundle:ios
```
The `bundle:ios` command uses the scripts in the `scripts` directory:
- `ios-archive.sh` - Archives the iOS app
- `ios-export.sh` - Exports the archived app
- `ios-upload.sh` - Uploads the app to App Store Connect (used by `bundle:ios:upload`)
## Project Structure
- `/android` - Android-specific code and configuration
- `/ios` - iOS-specific code and configuration
- `/src` - Main application source code
- `/app` - App initialization and configuration
- `/assets` - Static assets (images, fonts, animations)
- `/auth` - Authentication-related code
- `/biz` - Business logic and constants
- `/components` - Reusable UI components
- `/containers` - Container components
- `/data` - Data management
- `/events` - Event handling
- `/finders` - Search and finder utilities
- `/gql` - GraphQL queries and mutations
- `/hoc` - Higher-order components
- `/hooks` - Custom React hooks
- `/i18n` - Internationalization
- `/layout` - Layout components
- `/lib` - Library code
- `/location` - Location-related functionality
- `/misc` - Miscellaneous utilities
- `/navigation` - Navigation configuration
- `/network` - Network-related code
- `/notifications` - Notification handling
- `/permissions` - Permission handling
- `/scenes` - Scene components
- `/screens` - Screen components
- `/sentry` - Sentry error tracking configuration
- `/stores` - State management stores
- `/theme` - Styling and theming
- `/updates` - Update handling
- `/utils` - Utility functions
- `/docs` - Documentation files
- `/scripts` - Utility scripts for building, deployment, and development
- `/e2e` - End-to-end tests
## Contributing
Guidelines for contributing to the project:
1. Follow the code style and conventions used in the project
2. Write tests for new features
3. Update documentation as needed
4. Use the ESLint and Prettier configurations
## Troubleshooting
### Common Issues
#### Development Environment
- **Clearing Yarn Cache**: Use `yarn clean` (removes node_modules and reinstalls dependencies)
- **Cleaning Gradle**: Run `cd android && ./gradlew clean`
- **Clearing Gradle Cache**: Remove gradle caches with `rm -rf ~/.gradle/caches/ android/.gradle/`
- **Stopping Gradle Daemons**: Run `cd android && ./gradlew --stop`
- **Clearing ADB Cache**: Run `adb shell pm clear com.alertesecours`
- **Rebuilding Gradle**: Use `yarn expo run:android`
- **Rebuilding Expo React Native**: Use `yarn expo prebuild`
- **Clearing Metro Cache**: Use `yarn expo start --dev-client --clear`
#### Screenshots and Testing
- For Android screenshots: Use `scripts/screenshot-android.sh`
- For iOS screenshots: Use `scripts/screenshot-ios.sh`
- For Android emulator: Use `scripts/android-emulator`
#### Emulator Issues
- Clear cache / uninstall the app
- Check emulator datetime
- Check network connectivity
For more troubleshooting tips, see the documentation in the `/docs` directory.

280
README.md
View file

@ -1,252 +1,82 @@
# Alerte Secours
# Alerte Secours - Le Réflexe qui Sauve
A mobile application for handling alerts and emergency-related functionality, supporting both iOS and Android platforms.
[![Liberapay](https://img.shields.io/liberapay/receives/alerte-secours.svg?logo=liberapay)](https://liberapay.com/alerte-secours)
[![Buy Me a Coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?logo=buy-me-a-coffee)](https://buymeacoffee.com/alertesecours)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/alerte-secours?style=social)](https://github.com/sponsors/alerte-secours)
**Official Website:** [alerte-secours.fr](https://alerte-secours.fr)
Une application mobile pour la gestion des alertes et des fonctionnalités liées aux urgences, supportant les plateformes iOS et Android.
## Table of Contents
**Site Web Officiel :** [alerte-secours.fr](https://alerte-secours.fr)
- [Project Overview](#project-overview)
- [Technical Stack](#technical-stack)
- [Development Quick Start](#development-quick-start)
- [Installation](#installation)
- [Android](#android)
- [iOS](#ios)
- [Licensing](#licensing)
- [Project Structure](#project-structure)
- [Contributing](#contributing)
- [Troubleshooting](#troubleshooting)
## Aperçu du Projet
## Project Overview
Alerte Secours est une application mobile construite avec React Native qui gère les alertes et les fonctionnalités liées aux urgences. L'application supporte les plateformes iOS et Android et inclut des fonctionnalités telles que :
Alerte Secours is a mobile application built with React Native that handles alerts and emergency-related functionality. The app supports both iOS and Android platforms and includes features such as:
- Création et gestion d'alertes avec mises à jour en temps réel
- Fonctionnalités basées sur la localisation avec intégration cartographique
- Système de chat/messagerie avec salles de discussion spécifiques aux alertes
- Authentification via vérification SMS
- Liens profonds pour le partage d'alertes
- Notifications push
- Alert creation and management with real-time updates
- Location-based features with mapping integration
- Chat/Messaging system with alert-specific chat rooms
- Authentication via SMS verification
- Deep linking for alert sharing
- Push notifications
## Documentation Développeur
## Technical Stack
Pour les développeurs souhaitant contribuer au projet ou déployer l'application, consultez la [documentation technique complète](DEVELOPER.md) qui contient :
- React Native
- Expo framework
- GraphQL with Hasura
- Apollo Client for frontend
- Federated remote schemas
- Real-time subscriptions support
- Firebase Cloud Messaging (FCM) for push notifications only
- Sentry for error tracking
- MapLibre for mapping functionality
- Zustand for state management
- i18next for internationalization
- React Navigation for navigation
- Expo Updates for OTA updates
- Background Geolocation for location tracking
- Lottie for animations
- React Hook Form for form handling
- Axios for HTTP requests
- Yarn Berry as package manager
- ESLint and Prettier for code quality
- Fastlane for deployment automation
- Aperçu du projet et fonctionnalités
- Stack technique détaillé
- Guide de démarrage rapide
- Instructions d'installation (Android/iOS)
- Structure du projet
- Guide de dépannage
- Instructions de build et déploiement
## Development Quick Start
## Licence
### Prerequisites
Alerte Secours est sous licence **DevTheFuture Ethical Use License (DEF License)**. Points clés :
- Node.js (version specified in `.node-version`)
- Yarn package manager
- Android Studio (for Android development)
- Xcode (for iOS development)
- Physical device or emulator/simulator
### Usage à but non lucratif
- Licence perpétuelle, libre de redevances et non exclusive pour usage à but non lucratif
- Permet l'utilisation, la modification et la distribution à des fins non lucratives
### Environment Setup
### Usage commercial
- Nécessite l'obtention d'une licence payante
- Conditions déterminées par le Concédant (DevTheFuture.org)
1. Clone the repository
2. Install dependencies:
```bash
yarn
```
3. Copy the staging environment file:
```bash
cp .env.staging.example .env.staging
```
4. Start the development server with staging environment:
```bash
yarn start:staging
```
### Restrictions sur les données personnelles
- Ne doit pas être utilisé pour monétiser, vendre ou exploiter les données personnelles
- Les données personnelles ne peuvent pas être utilisées pour le marketing, la publicité ou l'influence politique
- L'agrégation de données n'est autorisée que si c'est une fonctionnalité explicite divulguée aux utilisateurs
### Running on Devices
### Restriction concurrentielle
- Les concurrents sont interdits d'utiliser le logiciel sans consentement explicite
#### Android
```bash
yarn android:staging
```
Pour le texte complet de la licence, voir [LICENSE.md](LICENSE.md).
#### iOS
```bash
yarn ios:staging
```
## 💙 Soutenir le projet
### Staging URLs
Alerte-Secours est une application mobile citoyenne, librement accessible, sans publicité ni exploitation de données.
The staging environment uses the following URLs:
Si vous souhaitez contribuer à son développement, sa maintenance et son indépendance :
- GraphQL API: `https://hasura-staging.alertesecours.fr/v1/graphql`
- WebSocket: `wss://hasura-staging.alertesecours.fr/v1/graphql`
- Files API: `https://files-staging.alertesecours.fr/api/v1/oas`
- Minio: `https://minio-staging.alertesecours.fr`
- Geolocation Sync: `https://api-staging.alertesecours.fr/api/v1/oas/geoloc/sync`
- 🟡 **[Liberapay Soutien régulier](https://liberapay.com/alerte-secours)**
Pour un soutien **récurrent et engagé**. Chaque don contribue à assurer la stabilité du service sur le long terme.
## Installation
- ☕ **[Buy Me a Coffee Don ponctuel](https://buymeacoffee.com/alertesecours)**
Pour un **coup de pouce ponctuel**, un café virtuel pour encourager le travail accompli !
### Android
- 🧑‍💻 **[GitHub Sponsors](https://github.com/sponsors/alerte-secours)**
Pour les développeurs et utilisateurs de GitHub : soutenez le projet directement via votre compte.
#### Using the Yarn Script
## Contribuer
The easiest way to install the app is to use the provided yarn script:
Directives pour contribuer au projet :
```bash
# Set the device ID (emulator or physical device)
export DEVICE=emulator-5554
1. Suivez le style de code et les conventions utilisées dans le projet
2. Écrivez des tests pour les nouvelles fonctionnalités
3. Mettez à jour la documentation si nécessaire
4. Utilisez les configurations ESLint et Prettier
# Run the installation script
yarn install:android
```
## Support
This script (`install-android.sh`) handles the entire installation process, including building APKs with signing, extracting them, and installing on the device.
#### Manual Installation
If you need to install the app manually, you can examine the `install-android.sh` script in the project root to see the detailed steps involved.
### iOS
#### Authentication Key Setup
1. Go to https://appstoreconnect.apple.com/access/integrations/api
2. Click the "+" button to generate a new API key
3. Give it a name (e.g., "AlerteSecours Build Key")
4. Download the .p8 file when prompted
5. Store the .p8 file in a secure location
6. Note down the Key ID and Issuer ID shown on the website
7. Set up environment variables:
```sh
export ASC_API_KEY_ID="YOUR_KEY_ID"
export ASC_API_ISSUER_ID="YOUR_ISSUER_ID"
export ASC_API_KEY_PATH="/path/to/your/AuthKey.p8"
```
#### Building and Running
To build and run the iOS app:
```bash
# Run in development mode with staging environment
yarn ios:staging
# Build for production (uses scripts/ios-archive.sh and scripts/ios-export.sh)
yarn bundle:ios
```
The `bundle:ios` command uses the scripts in the `scripts` directory:
- `ios-archive.sh` - Archives the iOS app
- `ios-export.sh` - Exports the archived app
- `ios-upload.sh` - Uploads the app to App Store Connect (used by `bundle:ios:upload`)
## Licensing
Alerte Secours is licensed under the DevTheFuture Ethical Use License (DEF License). Key points:
### Nonprofit Use
- Perpetual, royalty-free, non-exclusive license for nonprofit use
- Allows use, modification, and distribution for nonprofit purposes
### Profit Use
- Requires obtaining a paid license
- Terms determined by the Licensor (DevTheFuture.org)
### Personal Data Restrictions
- Must not be used to monetize, sell, or exploit personal data
- Personal data cannot be used for marketing, advertising, or political influence
- Data aggregation only allowed if it's an explicit feature disclosed to users
### Competitor Restriction
- Competitors are prohibited from using the software without explicit consent
For the full license text, see [LICENSE.md](LICENSE.md).
## Project Structure
- `/android` - Android-specific code and configuration
- `/ios` - iOS-specific code and configuration
- `/src` - Main application source code
- `/app` - App initialization and configuration
- `/assets` - Static assets (images, fonts, animations)
- `/auth` - Authentication-related code
- `/biz` - Business logic and constants
- `/components` - Reusable UI components
- `/containers` - Container components
- `/data` - Data management
- `/events` - Event handling
- `/finders` - Search and finder utilities
- `/gql` - GraphQL queries and mutations
- `/hoc` - Higher-order components
- `/hooks` - Custom React hooks
- `/i18n` - Internationalization
- `/layout` - Layout components
- `/lib` - Library code
- `/location` - Location-related functionality
- `/misc` - Miscellaneous utilities
- `/navigation` - Navigation configuration
- `/network` - Network-related code
- `/notifications` - Notification handling
- `/permissions` - Permission handling
- `/scenes` - Scene components
- `/screens` - Screen components
- `/sentry` - Sentry error tracking configuration
- `/stores` - State management stores
- `/theme` - Styling and theming
- `/updates` - Update handling
- `/utils` - Utility functions
- `/docs` - Documentation files
- `/scripts` - Utility scripts for building, deployment, and development
- `/e2e` - End-to-end tests
## Contributing
Guidelines for contributing to the project:
1. Follow the code style and conventions used in the project
2. Write tests for new features
3. Update documentation as needed
4. Use the ESLint and Prettier configurations
## Troubleshooting
### Common Issues
#### Development Environment
- **Clearing Yarn Cache**: Use `yarn clean` (removes node_modules and reinstalls dependencies)
- **Cleaning Gradle**: Run `cd android && ./gradlew clean`
- **Clearing Gradle Cache**: Remove gradle caches with `rm -rf ~/.gradle/caches/ android/.gradle/`
- **Stopping Gradle Daemons**: Run `cd android && ./gradlew --stop`
- **Clearing ADB Cache**: Run `adb shell pm clear com.alertesecours`
- **Rebuilding Gradle**: Use `yarn expo run:android`
- **Rebuilding Expo React Native**: Use `yarn expo prebuild`
- **Clearing Metro Cache**: Use `yarn expo start --dev-client --clear`
#### Screenshots and Testing
- For Android screenshots: Use `scripts/screenshot-android.sh`
- For iOS screenshots: Use `scripts/screenshot-ios.sh`
- For Android emulator: Use `scripts/android-emulator`
#### Emulator Issues
- Clear cache / uninstall the app
- Check emulator datetime
- Check network connectivity
For more troubleshooting tips, see the documentation in the `/docs` directory.
Pour obtenir de l'aide, veuillez ouvrir un ticket sur notre tracker d'issues ou consulter la documentation dans le répertoire `/docs`.

View file

@ -83,8 +83,8 @@ Project background_fetch = project(':react-native-background-fetch')
applicationId 'com.alertesecours'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 178
versionName "1.9.1"
versionCode 181
versionName "1.10.1"
multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.9.1</string>
<string>1.10.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -42,7 +42,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>178</string>
<string>181</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>

View file

@ -1,6 +1,6 @@
{
"name": "alerte-secours",
"version": "1.9.1",
"version": "1.10.1",
"main": "index.js",
"scripts": {
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
@ -49,8 +49,8 @@
"screenshot:android": "scripts/screenshot-android.sh"
},
"customExpoVersioning": {
"versionCode": 178,
"buildNumber": 178
"versionCode": 181,
"buildNumber": 181
},
"commit-and-tag-version": {
"scripts": {

View file

@ -1,2 +1,7 @@
#!/usr/bin/env bash
if [ -z "$DEVICE" ]; then
echo "Error: DEVICE environment variable is not set."
echo "Usage: DEVICE=emulator-5554 ./screenshot-android.sh"
exit 1
fi
exec adb -s $DEVICE exec-out screencap -p > screenshot-emulator-$(date +%s).png

View file

@ -9,11 +9,15 @@ const ALERT_FIELDS_FRAGMENT = gql`
radius
alertTag
location
initialLocation
createdAt
closedAt
address
what3Words
nearestPlace
lastAddress
lastWhat3Words
lastNearestPlace
username
code
notifiedCount # deprecated
@ -25,6 +29,7 @@ const ALERT_FIELDS_FRAGMENT = gql`
acknowledgedRelativeCount
acknowledgedAroundCount
acknowledgedConnectCount
followLocation
accessCode
userId
keepOpenAt

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -7,17 +7,15 @@ import useTimeDisplay from "~/hooks/useTimeDisplay";
export default function AlertInfoLineClosedTime({ alert, ...props }) {
const { closedAt } = alert;
const closedAtText = useTimeDisplay(closedAt);
if (!closedAt) {
return null;
}
return (
<AlertInfoLine
icon={() => (
<MaterialCommunityIcons name="clock-time-four-outline" size={24} />
)}
text={`Terminée ${closedAtText}`}
iconName={"clock-time-four-outline"}
labelText={`Terminée`}
valueText={closedAtText}
{...props}
/>
);

View file

@ -0,0 +1,51 @@
import React from "react";
import { View } from "react-native";
import Text from "~/components/Text";
import AlertInfoLineAddress from "~/containers/AlertInfoLines/Address";
import AlertInfoLineNear from "~/containers/AlertInfoLines/Near";
import AlertInfoLineW3w from "~/containers/AlertInfoLines/W3w";
/**
* LocationInfoSection component displays location information with a title
* and optional address, nearby location, and what3words information.
*
* @param {Object} props - Component props
* @param {string} props.title - Title to display for the location section
* @param {Object} props.alert - Alert object containing location information
* @param {boolean} [props.showAddress=true] - Whether to show the address information
* @param {boolean} [props.showNear=true] - Whether to show the nearby location information
* @param {boolean} [props.showW3w=true] - Whether to show the what3words information
* @param {Object} props.styles - Styles object containing locationSectionTitle and locationTitle styles
* @param {boolean} [props.useLastLocation=false] - Whether to use the last known location instead of current location
* @returns {React.ReactElement} The rendered component
*/
export default function LocationInfoSection({
title,
alert,
showAddress = true,
showNear = true,
showW3w = true,
styles,
useLastLocation = false,
}) {
// Create a modified alert object with the appropriate properties
const displayAlert = useLastLocation
? {
...alert,
address: alert.lastAddress,
nearestPlace: alert.lastNearLocation,
what3Words: alert.lastWhat3Words,
}
: alert;
return (
<>
<View style={styles.locationSectionTitle}>
<Text style={styles.locationTitle}>{title}</Text>
</View>
{showAddress && <AlertInfoLineAddress alert={displayAlert} />}
{showNear && <AlertInfoLineNear alert={displayAlert} />}
{showW3w && <AlertInfoLineW3w alert={displayAlert} />}
</>
);
}

View file

@ -8,6 +8,7 @@ import markerGreen from "~/assets/img/marker-green.png";
import markerRedDisabled from "~/assets/img/marker-red-disabled.png";
import markerYellowDisabled from "~/assets/img/marker-yellow-disabled.png";
import markerGreenDisabled from "~/assets/img/marker-green-disabled.png";
import markerOrigin from "~/assets/img/marker-origin.png";
const images = {
red: markerRed,
@ -16,6 +17,7 @@ const images = {
redDisabled: markerRedDisabled,
yellowDisabled: markerYellowDisabled,
greenDisabled: markerGreenDisabled,
origin: markerOrigin,
};
export default function FeatureImages() {

View file

@ -1,10 +1,15 @@
import React from "react";
import SelectedFeatureBubbleAlert from "./SelectedFeatureBubbleAlert";
import SelectedFeatureBubbleAlertInitial from "./SelectedFeatureBubbleAlertInitial";
export default function SelectedFeatureBubble(props) {
const { properties = {} } = props.feature;
if (properties.alert) {
// Check if this is an initial location marker
if (properties.isInitialLocation) {
return <SelectedFeatureBubbleAlertInitial {...props} />;
}
return <SelectedFeatureBubbleAlert {...props} />;
}
return null;

View file

@ -1,4 +1,5 @@
import React, { useCallback } from "react";
import React, { useCallback, useMemo } from "react";
import { deepEqual } from "fast-equals";
import { View } from "react-native";
import { IconButton, TouchableRipple } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
@ -63,6 +64,31 @@ export default function SelectedFeatureBubbleAlert({ feature, close }) {
/>
</View>
<View style={styles.content}>
{useMemo(() => {
const isLocationsDifferent =
alert.initialLocation &&
alert.location &&
!deepEqual(alert.initialLocation, alert.location);
if (isLocationsDifferent) {
return (
<View style={styles.titleContainer}>
<Text style={styles.titleText}>
{alert.followLocation
? "Localisation actuelle"
: "Dernière position connue"}
</Text>
</View>
);
}
return null;
}, [
alert.initialLocation,
alert.location,
alert.followLocation,
styles.titleContainer,
styles.titleText,
])}
<View style={styles.contentLine}>
<Text style={styles.contentText}>Sujet :</Text>
<Text style={[styles.contentTextValue, { color: levelColor }]}>
@ -123,6 +149,18 @@ const useStyles = createStyles(({ wp, fontSize, theme: { colors } }) => ({
content: {
paddingHorizontal: 4,
},
titleContainer: {
marginBottom: 8,
borderBottomWidth: 1,
borderBottomColor: colors.grey,
paddingBottom: 4,
},
titleText: {
color: colors.primary,
fontSize: 16,
fontWeight: "bold",
textAlign: "center",
},
contentLine: {
flexDirection: "row",
justifyContent: "space-between",

View file

@ -0,0 +1,185 @@
import React, { useCallback } from "react";
import { View } from "react-native";
import { IconButton, TouchableRipple } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import Maplibre from "@maplibre/maplibre-react-native";
import Text from "~/components/Text";
import useTimeDisplay from "~/hooks/useTimeDisplay";
import { useNavigation } from "@react-navigation/native";
import { createStyles, useTheme } from "~/theme";
import { alertActions } from "~/stores";
export default function SelectedFeatureBubbleAlertInitial({ feature, close }) {
const { properties = {} } = feature;
const { alert } = properties;
const styles = useStyles();
const { colors, custom } = useTheme();
const createdAtText = useTimeDisplay(alert.createdAt);
const navigation = useNavigation();
const goToAlert = useCallback(() => {
alertActions.setNavAlertCur({ alert });
navigation.navigate({
name: "AlertCur",
params: {
screen: "AlertCurTab",
params: {
screen: "AlertCurOverview",
},
},
});
}, [alert, navigation]);
const { level } = alert;
const levelColor = custom.appColors[level];
return (
<Maplibre.MarkerView
key={feature.properties.id}
id="selectedFeaturePointAnnotation"
aboveLayerID="lineLayer"
coordinate={feature.geometry.coordinates}
anchor={{ x: 0, y: 1 }}
>
<View style={styles.bubbleContainer}>
<View
style={{
flexDirection: "row",
justifyContent: "center",
alignSelf: "flex-end",
}}
>
<IconButton
size={14}
style={styles.closeButton}
icon={() => (
<MaterialCommunityIcons
name="close"
size={22}
style={styles.closeButtonIcon}
/>
)}
onPress={() => close()}
/>
</View>
<View style={styles.content}>
<View style={styles.titleContainer}>
<Text style={styles.titleText}>
Localisation initiale de l'Alerte
</Text>
</View>
<View style={styles.contentLine}>
<Text style={styles.contentText}>Sujet :</Text>
<Text style={[styles.contentTextValue, { color: levelColor }]}>
{alert.subject || "non indiqué"}
</Text>
</View>
<View style={styles.contentLine}>
<Text style={[styles.contentText]}>Code :</Text>
<Text style={[styles.contentTextValue]}>#{alert.code}</Text>
</View>
<View style={styles.contentLine}>
<Text style={styles.contentText}>Envoyée par :</Text>
<Text style={styles.contentTextValue}>{alert.username}</Text>
<Text style={styles.contentTextValue}>{createdAtText}</Text>
</View>
<View style={styles.contentLine}>
<Text style={styles.contentText}>Depuis l'adresse :</Text>
<Text style={styles.contentTextValue}>{alert.address}</Text>
</View>
<View style={styles.contentLine}>
<Text style={styles.contentText}>À proximité de :</Text>
<Text style={styles.contentTextValue}>{alert.nearestPlace}</Text>
</View>
<View style={styles.contentLine}>
<Text style={styles.contentText}>Localisation en 3 mots :</Text>
<Text style={styles.contentTextValue}>{alert.what3Words}</Text>
</View>
<TouchableRipple style={styles.alertLinkButton} onPress={goToAlert}>
<View style={styles.alertLinkContent}>
<MaterialCommunityIcons
name="adjust"
style={styles.alertLinkButtonIcon}
/>
<Text style={styles.alertLinkText}>Situation</Text>
</View>
</TouchableRipple>
</View>
</View>
</Maplibre.MarkerView>
);
}
const useStyles = createStyles(({ wp, fontSize, theme: { colors } }) => ({
bubbleContainer: {
backgroundColor: colors.surface,
justifyContent: "center",
alignItems: "flex-start",
paddingHorizontal: 4,
paddingVertical: 2,
borderRadius: 5,
width: wp(75),
},
bubbleText: {
color: colors.onSurface,
fontSize: 16,
},
closeButton: {},
closeButtonIcon: {
color: colors.grey,
},
content: {
paddingHorizontal: 4,
},
titleContainer: {
marginBottom: 8,
borderBottomWidth: 1,
borderBottomColor: colors.grey,
paddingBottom: 4,
},
titleText: {
color: colors.primary,
fontSize: 16,
fontWeight: "bold",
textAlign: "center",
},
contentLine: {
flexDirection: "row",
justifyContent: "space-between",
flexWrap: "wrap",
},
contentText: {
color: colors.onSurface,
fontSize: 15,
paddingRight: 5,
},
contentTextValue: {
color: colors.onSurfaceVariant,
fontSize: 15,
paddingRight: 5,
textAlign: "right",
flexGrow: 1,
},
alertLinkButton: {
marginTop: 15,
marginBottom: 5,
borderRadius: 0,
paddingVertical: 0,
backgroundColor: colors.primary,
},
alertLinkContent: {
height: 28,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
alertLinkText: {
color: colors.surface,
fontSize: 15,
},
alertLinkButtonIcon: {
color: colors.surface,
marginRight: 5,
fontSize: 15,
},
}));

View file

@ -12,6 +12,11 @@ const hitbox = {
height: HITBOX_SIZE,
};
const iconStyle = {
iconImage: ["get", "icon"],
iconSize: 0.5,
};
const useStyles = createStyles(({ theme: { colors } }) => ({
clusterCount: {
textField: "{point_count_abbreviated}",
@ -46,6 +51,13 @@ export default function ShapePoints({ shape, children, ...shapeSourceProps }) {
<AlertSymbolLayer level="yellow" isDisabled />
<AlertSymbolLayer level="green" isDisabled />
<Maplibre.SymbolLayer
filter={["==", ["get", "icon"], "origin"]}
key="points-origin"
id="points-origin"
style={iconStyle}
/>
{children}
</Maplibre.ShapeSource>
);

View file

@ -2,7 +2,11 @@ import { useEffect, useRef, useState } from "react";
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import { usePermissionWizardState, usePermissionsState } from "~/stores";
import {
usePermissionWizardState,
usePermissionsState,
useTreeState,
} from "~/stores";
import trackLocation from "~/location/trackLocation";
@ -12,6 +16,8 @@ const locationLogger = createLogger({
});
export default function useTrackLocation() {
const { splashScreenHidden } = useTreeState(["splashScreenHidden"]);
const { currentStep, completed } = usePermissionWizardState([
"completed",
"currentStep",
@ -34,7 +40,8 @@ export default function useTrackLocation() {
if (
locationBackground &&
motion &&
(currentStep === "tracking" || currentStep === "success" || completed)
(currentStep === "tracking" || currentStep === "success" || completed) &&
splashScreenHidden
) {
locationLogger.info("Enabling location tracking", {
step: currentStep,
@ -48,7 +55,7 @@ export default function useTrackLocation() {
step: currentStep,
});
}
}, [locationBackground, motion, currentStep, completed]);
}, [locationBackground, motion, currentStep, completed, splashScreenHidden]);
useEffect(() => {
if (trackLocationEnabled) {

View file

@ -9,6 +9,8 @@ import LayoutProviders from "~/layout/LayoutProviders";
import loadRessources from "~/layout/loadRessources";
import useMount from "~/hooks/useMount";
import { treeActions } from "~/stores";
SplashScreen.preventAutoHideAsync();
export default function AppView() {
@ -30,6 +32,7 @@ export default function AppView() {
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
treeActions.splashScreenHidden();
}
}, [appIsReady]);

View file

@ -1,5 +1,5 @@
// related to services/tasks/src/geocode/config.js
export const TRACK_MOVE = 100;
export const TRACK_MOVE = 10;
export const DEFAULT_DEVICE_RADIUS_ALL = 500;
export const DEFAULT_DEVICE_RADIUS_REACH = 25000;
export const MAX_BASEUSER_DEVICE_TRACKING = 25000;

View file

@ -21,6 +21,7 @@ import Relatives from "~/scenes/Relatives";
import Sheets from "~/scenes/Sheets";
import AlertAggListArchived from "~/scenes/AlertAggListArchived";
import About from "~/scenes/About";
import Contribute from "~/scenes/Contribute";
import Location from "~/scenes/Location";
import Developer from "~/scenes/Developer";
import HelpSignal from "~/scenes/HelpSignal";
@ -413,6 +414,22 @@ export default React.memo(function DrawerNav() {
}}
listeners={{}}
/>
<Drawer.Screen
name="Contribute"
component={Contribute}
options={{
drawerLabel: "Faire un don",
drawerIcon: ({ focused }) => (
<MaterialCommunityIcons
name="hand-heart"
{...iconProps}
{...(focused ? iconFocusedProps : {})}
/>
),
unmountOnBlur: true,
}}
listeners={{}}
/>
<Drawer.Screen
name="Sheets"
component={Sheets}

View file

@ -253,6 +253,12 @@ export default function HeaderRight(props) {
}}
/>
<Divider />
<Menu.Item
title="Faire un don"
onPress={() => {
navigateTo({ name: "Contribute" });
}}
/>
<Menu.Item
title="À Propos"
onPress={() => {

View file

@ -39,6 +39,8 @@ function getHeaderTitle(route) {
return "Alertes archivées";
case "About":
return "À Propos";
case "Contribute":
return "Faire un don";
case "Location":
return "Ma Localisation";
case "NotFound":

View file

@ -1,12 +1,20 @@
import React, { useState, useCallback, useEffect } from "react";
import { View, Image, ScrollView } from "react-native";
import {
View,
Image,
ScrollView,
TouchableOpacity,
Linking,
Alert,
} from "react-native";
import { Button } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import * as Updates from "expo-updates";
import { StatusBar } from "expo-status-bar";
import { MaterialIcons, AntDesign, FontAwesome } from "@expo/vector-icons";
import Text from "~/components/Text";
import { useTheme } from "~/theme";
import { useTheme, createStyles } from "~/theme";
import { paramsActions, useParamsState } from "~/stores";
@ -16,7 +24,24 @@ const logo = require("~/assets/img/logo192.png");
const version = require("../../../package.json").version;
const openURL = async (url) => {
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert("Erreur", "Impossible d'ouvrir ce lien");
}
} catch (error) {
Alert.alert(
"Erreur",
"Une erreur s'est produite lors de l'ouverture du lien",
);
}
};
export default function About() {
const navigation = useNavigation();
const textStyle = {
textAlign: "left",
fontSize: 16,
@ -153,6 +178,81 @@ export default function About() {
</Text>
</View>
</View>
{/* Website and Contribute Buttons */}
<View style={{ paddingHorizontal: 15, paddingVertical: 10 }}>
{/* Website Button */}
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.surface,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
marginBottom: 10,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
}}
onPress={() => openURL("https://alerte-secours.fr")}
>
<MaterialIcons
name="language"
size={24}
color={colors.primary}
style={{ marginRight: 10 }}
/>
<Text
style={{
color: colors.onSurface,
fontSize: 16,
fontWeight: "bold",
marginLeft: 5,
}}
>
Site officiel
</Text>
</TouchableOpacity>
{/* Contribute Button */}
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.surface,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
}}
onPress={() => navigation.navigate("Contribute")}
>
<MaterialIcons
name="favorite"
size={24}
color={colors.primary}
style={{ marginRight: 10 }}
/>
<Text
style={{
color: colors.onSurface,
fontSize: 16,
fontWeight: "bold",
marginLeft: 5,
}}
>
Contribuer au projet
</Text>
</TouchableOpacity>
</View>
<View style={{ padding: 15, justifyContent: "center" }}>
<Button
mode="text"

View file

@ -3,6 +3,7 @@ import { getDistance } from "geolib";
import Supercluster from "supercluster";
import useShallowMemo from "~/hooks/useShallowMemo";
import useShallowEffect from "~/hooks/useShallowEffect";
import { deepEqual } from "fast-equals";
export default function useFeatures({
clusterFeature,
@ -38,35 +39,67 @@ export default function useFeatures({
return computedList;
}, [alertingList, userCoords, hasUserCoords]);
const featureCollection = useShallowMemo(
() => ({
type: "FeatureCollection",
features: list.map((row) => {
const { alert } = row;
const { level, state } = alert;
const [longitude, latitude] = alert.location.coordinates;
const featureCollection = useShallowMemo(() => {
const features = list.map((row) => {
const { alert } = row;
const { level, state } = alert;
const [longitude, latitude] = alert.location.coordinates;
const coordinates = [longitude, latitude];
const id = `alert:${alert.id}`;
const icon = state === "open" ? level : `${level}Disabled`;
return {
type: "Feature",
id,
properties: {
id,
level,
icon,
alert,
coordinates,
},
geometry: {
type: "Point",
coordinates,
},
};
});
// Add initial location marker if locations are different
list.forEach((row) => {
const { alert } = row;
if (
alert.initialLocation &&
alert.location &&
!deepEqual(alert.initialLocation, alert.location)
) {
const [longitude, latitude] = alert.initialLocation.coordinates;
const coordinates = [longitude, latitude];
const id = `alert:${alert.id}`;
const icon = state === "open" ? level : `${level}Disabled`;
return {
const id = `alert:${alert.id}:initial`;
features.push({
type: "Feature",
id,
properties: {
id,
level,
icon,
icon: "origin",
level: alert.level,
alert,
coordinates,
isInitialLocation: true,
},
geometry: {
type: "Point",
coordinates,
},
};
}),
}),
[list],
);
});
}
});
return {
type: "FeatureCollection",
features,
};
}, [list]);
const superCluster = useShallowMemo(() => {
const cluster = new Supercluster({ radius: 40, maxZoom: 16 });

View file

@ -0,0 +1,232 @@
import { useCallback, useEffect, useState, useRef } from "react";
import { View, AppState } from "react-native";
import { Switch, Button } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useMutation } from "@apollo/client";
import * as Location from "expo-location";
import { UPDATE_ALERT_FOLLOW_LOCATION_MUTATION } from "./gql";
import Text from "~/components/Text";
import { createStyles, useTheme } from "~/theme";
import { usePermissionsState, permissionsActions } from "~/stores";
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
import openSettings from "~/lib/native/openSettings";
export default function FieldFollowLocation({ alert }) {
const styles = useStyles();
const { colors } = useTheme();
const { id: alertId } = alert;
const [updateAlertFollowLocationMutation] = useMutation(
UPDATE_ALERT_FOLLOW_LOCATION_MUTATION,
);
const { locationForeground, locationBackground } = usePermissionsState([
"locationForeground",
"locationBackground",
]);
const [followLocation, setFollowLocation] = useState(
alert.followLocation || false,
);
const [isRequestingPermissions, setIsRequestingPermissions] = useState(false);
const prevFollowLocation = useRef(alert.followLocation);
useEffect(() => {
if (prevFollowLocation.current !== alert.followLocation) {
prevFollowLocation.current = alert.followLocation;
setFollowLocation(alert.followLocation || false);
}
}, [alert.followLocation]);
// Check current permission status
const checkPermissionStatus = useCallback(async () => {
try {
const { status: fgStatus } =
await Location.getForegroundPermissionsAsync();
const { status: bgStatus } =
await Location.getBackgroundPermissionsAsync();
permissionsActions.setLocationForeground(fgStatus === "granted");
permissionsActions.setLocationBackground(bgStatus === "granted");
} catch (error) {
console.error("Error checking location permissions:", error);
}
}, []);
// Check permissions on mount
useEffect(() => {
checkPermissionStatus();
}, [checkPermissionStatus]);
// Listen for app state changes to refresh permissions when returning from settings
useEffect(() => {
const handleAppStateChange = (nextAppState) => {
if (nextAppState === "active") {
// App came to foreground, check permissions
checkPermissionStatus();
}
};
const subscription = AppState.addEventListener(
"change",
handleAppStateChange,
);
return () => {
subscription?.remove();
};
}, [checkPermissionStatus]);
const requestLocationPermissions = useCallback(async () => {
setIsRequestingPermissions(true);
try {
// Request foreground permission first
const foregroundGranted = await requestPermissionLocationForeground();
permissionsActions.setLocationForeground(foregroundGranted);
if (foregroundGranted) {
// Request background permission if foreground is granted
const backgroundGranted = await requestPermissionLocationBackground();
permissionsActions.setLocationBackground(backgroundGranted);
return backgroundGranted;
}
return false;
} catch (error) {
console.error("Error requesting location permissions:", error);
return false;
} finally {
setIsRequestingPermissions(false);
}
}, []);
const toggleFollowLocation = useCallback(
async (value) => {
if (value) {
// Check if permissions are granted when enabling
if (!locationForeground || !locationBackground) {
const permissionsGranted = await requestLocationPermissions();
if (!permissionsGranted) {
// Don't enable if permissions weren't granted
return;
}
}
}
setFollowLocation(value);
updateAlertFollowLocationMutation({
variables: { alertId, followLocation: value },
});
},
[
alertId,
updateAlertFollowLocationMutation,
locationForeground,
locationBackground,
requestLocationPermissions,
],
);
const hasRequiredPermissions = locationForeground && locationBackground;
const showPermissionWarning = followLocation && !hasRequiredPermissions;
return (
<View style={styles.container}>
<View style={styles.content}>
<MaterialCommunityIcons
name="crosshairs-gps"
style={styles.icon}
size={20}
/>
<Text style={styles.label}>Suivre ma localisation</Text>
<Switch
value={followLocation}
onValueChange={toggleFollowLocation}
color={colors.primary}
disabled={isRequestingPermissions}
/>
</View>
{showPermissionWarning && (
<View style={styles.warningContainer}>
<View style={styles.warningContent}>
<MaterialCommunityIcons
name="alert-circle-outline"
style={styles.warningIcon}
size={16}
/>
<Text style={styles.warningText}>
Permissions de localisation requises
</Text>
</View>
<Button
mode="outlined"
onPress={openSettings}
style={styles.settingsButton}
labelStyle={styles.settingsButtonLabel}
compact
>
Paramétrer
</Button>
</View>
)}
</View>
);
}
const useStyles = createStyles(
({ wp, hp, scaleText, fontSize, theme: { colors } }) => ({
container: {
marginVertical: hp(1),
paddingHorizontal: wp(4),
paddingVertical: hp(1.5),
backgroundColor: colors.surface,
borderRadius: 8,
borderWidth: 1,
borderColor: colors.outline,
},
content: {
flexDirection: "row",
alignItems: "center",
},
icon: {
color: colors.primary,
marginRight: wp(2),
},
label: {
flex: 1,
...scaleText({ fontSize: 16 }),
color: colors.onSurface,
},
warningContainer: {
marginTop: hp(1),
paddingTop: hp(1),
borderTopWidth: 1,
borderTopColor: colors.outline,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
warningContent: {
flexDirection: "row",
alignItems: "center",
flex: 1,
},
warningIcon: {
color: colors.error,
marginRight: wp(1),
},
warningText: {
...scaleText({ fontSize: 14 }),
color: colors.error,
flex: 1,
},
settingsButton: {
marginLeft: wp(2),
},
settingsButtonLabel: {
...scaleText({ fontSize: 12 }),
},
}),
);

View file

@ -60,3 +60,17 @@ export const UPDATE_ALERT_SUBJECT_MUTATION = gql`
}
}
`;
export const UPDATE_ALERT_FOLLOW_LOCATION_MUTATION = gql`
mutation updateAlertFollowLocation(
$alertId: Int!
$followLocation: Boolean!
) {
updateOneAlert(
pk_columns: { id: $alertId }
_set: { followLocation: $followLocation }
) {
id
}
}
`;

View file

@ -1,8 +1,9 @@
import React, { useCallback, useState } from "react";
import React, { useCallback, useState, useMemo } from "react";
import { View, ImageBackground, ScrollView } from "react-native";
import { TouchableRipple, Button, Title } from "react-native-paper";
import { useIsFocused, useNavigation } from "@react-navigation/native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { deepEqual } from "fast-equals";
import withConnectivity from "~/hoc/withConnectivity";
@ -21,6 +22,7 @@ import alertBigButtonBgMessagesGrey from "~/assets/img/alert-big-button-bg-messa
import Text from "~/components/Text";
import AlertInfoLineLevel from "~/containers/AlertInfoLines/Level";
import LocationInfoSection from "~/containers/LocationInfoSection";
import AlertInfoLineCode from "~/containers/AlertInfoLines/Code";
import AlertInfoLineDistance from "~/containers/AlertInfoLines/Distance";
import AlertInfoLineCreatedTime from "~/containers/AlertInfoLines/CreatedTime";
@ -50,6 +52,7 @@ import { useMutation } from "@apollo/client";
import FieldLevel from "./FieldLevel";
import FieldSubject from "./FieldSubject";
import FieldFollowLocation from "./FieldFollowLocation";
import MapLinksButton from "~/containers/MapLinksButton";
import { useTheme } from "~/theme";
@ -197,6 +200,14 @@ export default withConnectivity(
const [parentScrollEnabled, setParentScrollEnabled] = useState(true);
const isLocationsDifferent = useMemo(() => {
return (
alert.initialLocation &&
alert.location &&
!deepEqual(alert.initialLocation, alert.location)
);
}, [alert.initialLocation, alert.location]);
// if (!isFocused) {
// return null;
// }
@ -497,6 +508,8 @@ export default withConnectivity(
alert={alert}
setParentScrollEnabled={setParentScrollEnabled}
/>
<FieldFollowLocation alert={alert} />
</View>
)}
@ -505,15 +518,49 @@ export default withConnectivity(
<AlertInfoLineLevel alert={alert} isFirst />
)}
<AlertInfoLineCode alert={alert} />
<AlertInfoLineDistance alert={alert} />
{!isSent && <AlertInfoLineDistance alert={alert} />}
<AlertInfoLineCreatedTime alert={alert} />
<AlertInfoLineClosedTime alert={alert} />
{(!isSent || !isOpen) && <AlertInfoLineSubject alert={alert} />}
<AlertInfoLineAddress alert={alert} />
<AlertInfoLineNear alert={alert} />
<AlertInfoLineW3w alert={alert} />
<AlertInfoLineRadius alert={alert} />
<AlertInfoLineSentBy alert={alert} />
{useMemo(() => {
if (isLocationsDifferent) {
return (
<>
<LocationInfoSection
title="Position initiale"
alert={alert}
styles={styles}
/>
<LocationInfoSection
title={
alert.followLocation
? "Position actuelle"
: "Dernière position connue"
}
alert={alert}
useLastLocation={true}
styles={styles}
/>
</>
);
} else {
return (
<>
<AlertInfoLineAddress alert={alert} />
<AlertInfoLineNear alert={alert} />
<AlertInfoLineW3w alert={alert} />
</>
);
}
}, [alert, styles, isLocationsDifferent])}
<View
style={isLocationsDifferent ? styles.locationSeparator : null}
>
<AlertInfoLineRadius alert={alert} />
<AlertInfoLineSentBy alert={alert} />
</View>
</View>
</View>
</ScrollView>

View file

@ -153,4 +153,20 @@ export default createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
lineHeight: 18,
textAlign: "center",
},
locationSectionTitle: {
backgroundColor: colors.background,
paddingVertical: 8,
paddingHorizontal: 10,
marginTop: 5,
},
locationTitle: {
fontSize: 16,
fontWeight: "bold",
color: colors.primary,
},
locationSeparator: {
marginTop: 15,
borderTopWidth: 5,
borderColor: colors.background,
},
}));

View file

@ -0,0 +1,282 @@
import React from "react";
import {
View,
ScrollView,
Linking,
Alert,
TouchableOpacity,
} from "react-native";
import { MaterialIcons, AntDesign } from "@expo/vector-icons";
import Text from "~/components/Text";
import { createStyles, useTheme } from "~/theme";
const openURL = async (url) => {
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert("Erreur", "Impossible d'ouvrir ce lien");
}
} catch (error) {
Alert.alert(
"Erreur",
"Une erreur s'est produite lors de l'ouverture du lien",
);
}
};
export default function Contribute() {
const styles = useStyles();
const { colors } = useTheme();
return (
<ScrollView style={{ flex: 1 }}>
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<MaterialIcons name="favorite" size={28} color={colors.primary} />
<Text style={styles.title}>Contribuer au projet</Text>
</View>
{/* Description */}
<Text style={styles.description}>
Alerte-Secours est une application mobile citoyenne, gratuite, sans
publicité ni exploitation de données.
{"\n\n"}
Si vous souhaitez contribuer à son développement, sa maintenance et
son indépendance :
</Text>
{/* Liberapay Button */}
<TouchableOpacity
style={[
styles.donationButton,
styles.buttonContent,
styles.liberapayButton,
]}
onPress={() => openURL("https://liberapay.com/alerte-secours")}
activeOpacity={0.8}
>
<View style={styles.iconContainer}>
<MaterialIcons name="circle" style={styles.iconDonation} />
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.buttonLabel}>Liberapay Soutien régulier</Text>
<Text style={styles.buttonDescription}>
Pour un soutien récurrent et engagé. Chaque don contribue à
assurer la stabilité du service sur le long terme.
</Text>
</View>
</TouchableOpacity>
{/* Buy Me a Coffee Button */}
<TouchableOpacity
style={[
styles.donationButton,
styles.buttonContent,
styles.buymeacoffeeButton,
]}
onPress={() => openURL("https://buymeacoffee.com/alertesecours")}
activeOpacity={0.8}
>
<View style={styles.iconContainer}>
<MaterialIcons name="local-cafe" style={styles.iconDonation} />
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.buttonLabel}>
Buy Me a Coffee Don ponctuel
</Text>
<Text style={styles.buttonDescription}>
Pour un coup de pouce ponctuel, un café virtuel pour encourager le
travail accompli !
</Text>
</View>
</TouchableOpacity>
{/* GitHub Sponsors Button */}
<TouchableOpacity
style={[
styles.donationButton,
styles.buttonContent,
styles.githubButton,
]}
onPress={() => openURL("https://github.com/sponsors/alerte-secours")}
activeOpacity={0.8}
>
<View style={styles.iconContainer}>
<AntDesign name="github" style={styles.iconDonation} />
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.buttonLabel}>GitHub Sponsors</Text>
<Text style={styles.buttonDescription}>
Pour les développeurs et utilisateurs de GitHub : soutenez le
projet directement via votre compte.
</Text>
</View>
</TouchableOpacity>
{/* Collaboration Section */}
<View style={styles.collaborationSection}>
<Text style={styles.collaborationTitle}>
Vous souhaitez nous rejoindre
</Text>
<Text style={styles.collaborationDescription}>
Ce projet fait sens pour vous et vous souhaitez vous engager dans un
projet d'avenir ?{"\n\n"}
Alerte-Secours est à la recherche de collaborateurs avec des
compétences dans le développement, la gestion de projet, le
business, la com, le graphisme. Contactez-nous si vous souhaitez
faire partie de l'aventure.
</Text>
<TouchableOpacity
style={styles.contactButton}
onPress={() => openURL("mailto:contact@alertesecours.fr")}
activeOpacity={0.8}
>
<MaterialIcons name="email" style={styles.contactIcon} />
<Text style={styles.contactText}>contact@alertesecours.fr</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
);
}
const useStyles = createStyles(({ theme: { colors, custom } }) => ({
container: {
flex: 1,
padding: 20,
},
header: {
flexDirection: "row",
alignItems: "center",
marginBottom: 24,
},
title: {
fontSize: 26,
fontWeight: "bold",
color: colors.primary,
marginLeft: 12,
},
description: {
fontSize: 16,
lineHeight: 26,
color: colors.onBackground,
marginBottom: 36,
textAlign: "left",
},
donationButton: {
marginVertical: 10,
borderRadius: 16,
elevation: 3,
shadowColor: colors.shadow,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.15,
shadowRadius: 4,
},
buttonContent: {
minHeight: 64,
paddingVertical: 16,
paddingHorizontal: 20,
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
},
buttonLabel: {
fontSize: 17,
fontWeight: "700",
textAlign: "left",
marginBottom: 4,
color: custom.donation.onDonation,
},
buttonDescription: {
fontSize: 14,
opacity: 0.9,
textAlign: "left",
lineHeight: 20,
color: custom.donation.onDonation,
},
iconContainer: {
marginRight: 18,
width: 32,
height: 32,
alignItems: "center",
justifyContent: "center",
borderRadius: 16,
backgroundColor: "rgba(255, 255, 255, 0.2)",
},
buttonTextContainer: {
flex: 1,
alignItems: "flex-start",
},
// Icon styles
iconDonation: {
fontSize: 22,
color: custom.donation.onDonation,
},
// Button background styles
liberapayButton: {
backgroundColor: custom.donation.liberapay,
},
buymeacoffeeButton: {
backgroundColor: custom.donation.buymeacoffee,
},
githubButton: {
backgroundColor: custom.donation.github,
},
// Collaboration section styles
collaborationSection: {
marginTop: 40,
padding: 20,
backgroundColor: colors.surfaceVariant,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.outline,
},
collaborationTitle: {
fontSize: 22,
fontWeight: "bold",
color: colors.primary,
marginBottom: 16,
textAlign: "center",
},
collaborationDescription: {
fontSize: 16,
lineHeight: 24,
color: colors.onSurfaceVariant,
marginBottom: 20,
textAlign: "left",
},
contactButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.primary,
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
elevation: 2,
shadowColor: colors.shadow,
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.1,
shadowRadius: 2,
},
contactIcon: {
fontSize: 20,
color: colors.onPrimary,
marginRight: 8,
},
contactText: {
fontSize: 16,
fontWeight: "600",
color: colors.onPrimary,
},
}));

View file

@ -0,0 +1,58 @@
import React from "react";
import { TouchableOpacity, View } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { createStyles } from "~/theme";
import Text from "~/components/Text";
import { MaterialIcons } from "@expo/vector-icons";
export default function ContributeButton() {
const navigation = useNavigation();
const styles = useStyles();
return (
<View>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.navigate("Contribute")}
>
<MaterialIcons
name="favorite"
size={24}
color={styles.icon.color}
style={styles.icon}
/>
<Text style={styles.buttonText}>Contribuer au projet</Text>
</TouchableOpacity>
</View>
);
}
const useStyles = createStyles(({ theme: { colors } }) => ({
button: {
flexDirection: "row",
alignItems: "center",
alignSelf: "center",
marginTop: 20,
marginBottom: 10,
backgroundColor: colors.surface,
borderRadius: 8,
width: "100%",
paddingVertical: 12,
paddingHorizontal: 16,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
buttonText: {
color: colors.onSurface,
fontSize: 16,
fontWeight: "bold",
marginLeft: 5,
},
icon: {
color: colors.primary,
marginRight: 10,
},
}));

View file

@ -12,6 +12,7 @@ import { createStyles, fontFamily } from "~/theme";
import HelpBlock from "./HelpBlock";
import RegisterRelativesButton from "./RegisterRelativesButton";
import NotificationsButton from "./NotificationsButton";
import ContributeButton from "./ContributeButton";
export default function SendAlert() {
const navigation = useNavigation();
@ -215,6 +216,8 @@ export default function SendAlert() {
{capitalize(levelLabel.call)}
</Button>
</View>
<ContributeButton />
</View>
</ScrollView>
);

View file

@ -22,6 +22,7 @@ export default function FieldNotifySelector() {
const callEmergency = watch("callEmergency");
const notifyAround = watch("notifyAround");
const notifyRelatives = watch("notifyRelatives");
const followLocation = watch("followLocation");
const level = watch("level");
const checkedColor = colors.primary;
@ -106,6 +107,29 @@ export default function FieldNotifySelector() {
}}
/>
</View>
<View style={styles.followLocationContainer}>
<CheckboxItem
status={followLocation ? "checked" : "unchecked"}
style={styles.checkboxItem}
labelStyle={styles.checkboxLabel}
size={styleOptions.checkboxItem.size}
icon={() => (
<MaterialCommunityIcons
name="crosshairs-gps"
style={styles.checkboxIcon}
onPress={() => {
setValue("followLocation", !followLocation);
}}
/>
)}
color={checkedColor}
uncheckedColor={uncheckedColor}
label="Suivre ma localisation"
onPress={() => {
setValue("followLocation", !followLocation);
}}
/>
</View>
</View>
);
}
@ -122,6 +146,7 @@ const useStyles = createStyles(
({ wp, hp, scaleText, fontSize, theme: { colors, textShadowForWhite } }) => ({
container: {
marginTop: hp(2),
marginBottom: hp(3),
},
checkboxItemContainer: {
borderRadius: 4,
@ -130,6 +155,13 @@ const useStyles = createStyles(
marginVertical: hp(0.2),
backgroundColor: colors.surface,
},
followLocationContainer: {
borderRadius: 4,
borderWidth: 1,
borderColor: colors.outline,
marginVertical: hp(4),
backgroundColor: colors.surface,
},
checkboxItem: {
paddingHorizontal: 6,
},

View file

@ -9,7 +9,8 @@ export default function SendAlertConfirm({ route }) {
const { alert } = params;
const level = alert?.level || params.level;
const callEmergency = params.forceCallEmergency || level === "red";
const callEmergency =
params.forceCallEmergency || level === "red" || level === "yellow";
const methods = useForm({
defaultValues: {
@ -18,6 +19,7 @@ export default function SendAlertConfirm({ route }) {
callEmergency,
notifyAround: true,
notifyRelatives: true,
followLocation: true,
},
});

View file

@ -33,8 +33,14 @@ async function onSubmit(args, context) {
const [alertInput] = args;
const { navigation } = context;
const { subject, level, callEmergency, notifyAround, notifyRelatives } =
alertInput;
const {
subject,
level,
callEmergency,
notifyAround,
notifyRelatives,
followLocation,
} = alertInput;
const coords = await getCurrentLocation();
@ -62,6 +68,7 @@ async function onSubmit(args, context) {
callEmergency,
notifyAround,
notifyRelatives,
followLocation: !!followLocation,
location,
accuracy,
altitude,

View file

@ -49,15 +49,21 @@ export default createAtom(({ merge, getActions }) => {
merge({ suspend: true });
};
const splashScreenHidden = () => {
merge({ splashScreenHidden: true });
};
return {
default: {
triggerReload: false,
suspend: false,
splashScreenHidden: false,
},
actions: {
triggerReload,
suspendTree,
onReload,
splashScreenHidden,
},
};
});

View file

@ -96,6 +96,13 @@ const ThemeDark = {
call: "#4c6ef5",
onColor: "#FFFFFF",
},
donation: {
liberapay: "#f59f00", // Same as colors.warn
buymeacoffee: "#40c057", // Same as colors.ok
github: "#b3c4ff", // Same as colors.primary (dark theme)
onDonation: "#FFFFFF",
},
},
};
export default ThemeDark;

View file

@ -95,6 +95,13 @@ const ThemeLight = {
call: "#4c6ef5",
onColor: "#FFFFFF",
},
donation: {
liberapay: "#f59f00", // Same as colors.warn
buymeacoffee: "#40c057", // Same as colors.ok
github: "#1864ab", // Same as colors.primary
onDonation: "#FFFFFF",
},
},
};