Compare commits
14 commits
Author | SHA1 | Date | |
---|---|---|---|
236121a73c | |||
cbd1803dc0 | |||
de560bd1e5 | |||
8487573c0f | |||
958eee1f72 | |||
a461f445c4 | |||
28b8b3d826 | |||
8d8da91696 | |||
d6a3e94ea7 | |||
d5ad23d1da | |||
ef9b5037fb | |||
0b5e936714 | |||
bce32dbd55 | |||
69d9fc9a6a |
27 changed files with 597 additions and 143 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -84,6 +84,9 @@ DerivedData
|
|||
# aidigest
|
||||
codebase.md
|
||||
|
||||
# Build logs
|
||||
logs/
|
||||
|
||||
# Sensitive configuration files
|
||||
ios/GoogleService-Info.plist
|
||||
ios/AlerteSecours/GoogleService-Info.plist
|
||||
|
@ -96,4 +99,4 @@ android/app/google-services.json
|
|||
!ios/AlerteSecours/Supporting/Expo.example.plist
|
||||
!android/app/google-services.example.json
|
||||
|
||||
screenshot-*.png
|
||||
screenshot-*.png
|
||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -2,6 +2,34 @@
|
|||
|
||||
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.12.3](https://github.com/alerte-secours/as-app/compare/v1.12.2...v1.12.3) (2025-09-05)
|
||||
|
||||
## [1.12.2](https://github.com/alerte-secours/as-app/compare/v1.12.1...v1.12.2) (2025-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 112 ([8487573](https://github.com/alerte-secours/as-app/commit/8487573c0f8eb6656cd5825ff217efe046d65407))
|
||||
|
||||
## [1.12.1](https://github.com/alerte-secours/as-app/compare/v1.12.0...v1.12.1) (2025-08-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* menu index typo ([8d8da91](https://github.com/alerte-secours/as-app/commit/8d8da916965c1dd7feaa8b011ea854591c859e03))
|
||||
* placeholder in dark theme for chat input ([d5ad23d](https://github.com/alerte-secours/as-app/commit/d5ad23d1dae521e4a99f757505adec3f23a7914c))
|
||||
* **push-notif:** label "undefined à " ([ef9b503](https://github.com/alerte-secours/as-app/commit/ef9b5037fbb97fa597194760a9d3a22a04eeeeda))
|
||||
* **theming:** alertes archivées buttons ([d6a3e94](https://github.com/alerte-secours/as-app/commit/d6a3e94ea710a494623a8636ccad71205094d9c6))
|
||||
* typo ([0b5e936](https://github.com/alerte-secours/as-app/commit/0b5e936714054fe8647b8520d6432ab29fe2ecb7))
|
||||
* **voice-message:** wip ([28b8b3d](https://github.com/alerte-secours/as-app/commit/28b8b3d826685de053ae5ff7f7931b24a64920b4))
|
||||
|
||||
## [1.12.0](https://github.com/alerte-secours/as-app/compare/v1.11.17...v1.12.0) (2025-08-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **heartbeat:** remove ([69d9fc9](https://github.com/alerte-secours/as-app/commit/69d9fc9a6a1383bbbf5f1b5f5641d01b2378168b))
|
||||
|
||||
## [1.11.17](https://github.com/alerte-secours/as-app/compare/v1.11.16...v1.11.17) (2025-07-30)
|
||||
|
||||
## [1.11.16](https://github.com/alerte-secours/as-app/compare/v1.11.15...v1.11.16) (2025-07-27)
|
||||
|
|
|
@ -83,8 +83,8 @@ android {
|
|||
applicationId 'com.alertesecours'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 207
|
||||
versionName "1.11.17"
|
||||
versionCode 211
|
||||
versionName "1.12.3"
|
||||
multiDexEnabled true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
|
||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
|
@ -10,6 +11,7 @@
|
|||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
|
40
index.js
40
index.js
|
@ -20,7 +20,7 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
|
|||
import onMessageReceived from "~/notifications/onMessageReceived";
|
||||
|
||||
import { createLogger } from "~/lib/logger";
|
||||
import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||
// import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||
|
||||
// setup notification, this have to stay in index.js
|
||||
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
||||
|
@ -36,23 +36,23 @@ const geolocBgLogger = createLogger({
|
|||
task: "headless",
|
||||
});
|
||||
|
||||
const HeadlessTask = async (event) => {
|
||||
try {
|
||||
switch (event?.name) {
|
||||
case "heartbeat":
|
||||
await executeHeartbeatSync();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
geolocBgLogger.error("HeadlessTask error", {
|
||||
error,
|
||||
event,
|
||||
});
|
||||
}
|
||||
};
|
||||
// const HeadlessTask = async (event) => {
|
||||
// try {
|
||||
// switch (event?.name) {
|
||||
// case "heartbeat":
|
||||
// await executeHeartbeatSync();
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// geolocBgLogger.error("HeadlessTask error", {
|
||||
// error,
|
||||
// event,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
||||
}
|
||||
// if (Platform.OS === "android") {
|
||||
// BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
||||
// }
|
||||
|
|
|
@ -163,7 +163,12 @@
|
|||
8EC12A68941D40E98E0D60BE /* Fix Xcode 15 Bug */,
|
||||
49AEAB1D332B45ED9A37B009 /* Fix Xcode 15 Bug */,
|
||||
D75A41050AB3445786799848 /* Fix Xcode 15 Bug */,
|
||||
ABC6C5A0D48A4B7980D60E1B /* Remove signature files (Xcode workaround) */,
|
||||
FB7FA195D27D412AA897F419 /* Fix Xcode 15 Bug */,
|
||||
0C44FF6DBD8F4BDD8D2B9784 /* Fix Xcode 15 Bug */,
|
||||
AC008438EEF4422BA1C35CDF /* Fix Xcode 15 Bug */,
|
||||
1A6C945D28C14747A29A3560 /* Fix Xcode 15 Bug */,
|
||||
1C287A64431A4C0A859F067B /* Fix Xcode 15 Bug */,
|
||||
EBD8BAB94522461484E3792D /* Remove signature files (Xcode workaround) */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -576,6 +581,176 @@ fi";
|
|||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
FB7FA195D27D412AA897F419 /* Fix Xcode 15 Bug */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Fix Xcode 15 Bug";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
||||
fi";
|
||||
};
|
||||
B1FDDB484A8E497F9FF7F32C /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
0C44FF6DBD8F4BDD8D2B9784 /* Fix Xcode 15 Bug */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Fix Xcode 15 Bug";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
||||
fi";
|
||||
};
|
||||
658BC0C976C44270ACBDF3C6 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
AC008438EEF4422BA1C35CDF /* Fix Xcode 15 Bug */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Fix Xcode 15 Bug";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
||||
fi";
|
||||
};
|
||||
6743177E81F94D198E926A21 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
1A6C945D28C14747A29A3560 /* Fix Xcode 15 Bug */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Fix Xcode 15 Bug";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
||||
fi";
|
||||
};
|
||||
96170835D29D4D569C60B051 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
1C287A64431A4C0A859F067B /* Fix Xcode 15 Bug */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Fix Xcode 15 Bug";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
||||
fi";
|
||||
};
|
||||
EBD8BAB94522461484E3792D /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.11.17</string>
|
||||
<string>1.12.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
@ -48,7 +48,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>207</string>
|
||||
<string>211</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "alerte-secours",
|
||||
"version": "1.11.17",
|
||||
"version": "1.12.3",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
||||
|
@ -50,8 +50,8 @@
|
|||
"screenshot:android": "scripts/screenshot-android.sh"
|
||||
},
|
||||
"customExpoVersioning": {
|
||||
"versionCode": 207,
|
||||
"buildNumber": 207
|
||||
"versionCode": 211,
|
||||
"buildNumber": 211
|
||||
},
|
||||
"commit-and-tag-version": {
|
||||
"scripts": {
|
||||
|
|
|
@ -86,6 +86,9 @@ mv ios/main.jsbundle.hbc ios/main.jsbundle
|
|||
|
||||
cd ios
|
||||
|
||||
# Create logs directory if it doesn't exist
|
||||
mkdir -p ../logs
|
||||
|
||||
# Create archive using xcodebuild
|
||||
echo "Creating archive..."
|
||||
xcodebuild \
|
||||
|
@ -93,6 +96,6 @@ xcodebuild \
|
|||
-scheme AlerteSecours \
|
||||
-configuration Release \
|
||||
-archivePath AlerteSecours.xcarchive \
|
||||
archive
|
||||
archive 2>&1 | tee "../logs/ios-archive-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
echo "Archive completed successfully at AlerteSecours.xcarchive"
|
||||
|
|
|
@ -25,7 +25,7 @@ import { useUpdates } from "~/updates";
|
|||
import Error from "~/components/Error";
|
||||
|
||||
import useTrackLocation from "~/hooks/useTrackLocation";
|
||||
import { initializeBackgroundFetch } from "~/services/backgroundFetch";
|
||||
// import { initializeBackgroundFetch } from "~/services/backgroundFetch";
|
||||
import useMount from "~/hooks/useMount";
|
||||
|
||||
const appLogger = createLogger({
|
||||
|
@ -221,22 +221,22 @@ function AppContent() {
|
|||
useNetworkListener();
|
||||
useTrackLocation();
|
||||
|
||||
useMount(() => {
|
||||
const setupBackgroundFetch = async () => {
|
||||
try {
|
||||
appLogger.info("Setting up BackgroundFetch");
|
||||
await initializeBackgroundFetch();
|
||||
appLogger.debug("BackgroundFetch setup completed");
|
||||
} catch (error) {
|
||||
lifecycleLogger.error("BackgroundFetch setup failed", {
|
||||
error: error?.message,
|
||||
});
|
||||
errorHandler(error);
|
||||
}
|
||||
};
|
||||
// useMount(() => {
|
||||
// const setupBackgroundFetch = async () => {
|
||||
// try {
|
||||
// appLogger.info("Setting up BackgroundFetch");
|
||||
// await initializeBackgroundFetch();
|
||||
// appLogger.debug("BackgroundFetch setup completed");
|
||||
// } catch (error) {
|
||||
// lifecycleLogger.error("BackgroundFetch setup failed", {
|
||||
// error: error?.message,
|
||||
// });
|
||||
// errorHandler(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
setupBackgroundFetch();
|
||||
});
|
||||
// setupBackgroundFetch();
|
||||
// });
|
||||
|
||||
// Handle deep links after app is initialized with error handling
|
||||
useEffect(() => {
|
||||
|
|
|
@ -57,7 +57,7 @@ export default React.memo(function TextArea({
|
|||
autoFocus={autoFocus}
|
||||
showSoftInputOnFocus={keyboardEnabled} // controlled by state
|
||||
placeholder="Message"
|
||||
placeholderTextColor={colors.onBackgroundDisabled}
|
||||
placeholderTextColor={colors.placeholder}
|
||||
onTouchStart={() => {
|
||||
if (!keyboardEnabled) {
|
||||
setKeyboardEnabled(true);
|
||||
|
|
|
@ -38,16 +38,16 @@ const recordingSettings = {
|
|||
outputFormat: AndroidOutputFormat.MPEG_4,
|
||||
audioEncoder: AndroidAudioEncoder.AAC,
|
||||
sampleRate: 44100,
|
||||
numberOfChannels: 2,
|
||||
bitRate: 128000,
|
||||
numberOfChannels: 1,
|
||||
bitRate: 64000,
|
||||
},
|
||||
ios: {
|
||||
extension: ".m4a",
|
||||
outputFormat: IOSOutputFormat.MPEG4AAC,
|
||||
audioQuality: IOSAudioQuality.MAX,
|
||||
sampleRate: 44100,
|
||||
numberOfChannels: 2,
|
||||
bitRate: 128000,
|
||||
numberOfChannels: 1,
|
||||
bitRate: 64000,
|
||||
linearPCMBitDepth: 16,
|
||||
linearPCMIsBigEndian: false,
|
||||
linearPCMIsFloat: false,
|
||||
|
@ -194,7 +194,7 @@ export default React.memo(function ChatInput({
|
|||
staysActiveInBackground: true,
|
||||
});
|
||||
const { sound: _sound } = await recording.createNewLoadedSoundAsync({
|
||||
isLooping: true,
|
||||
isLooping: false,
|
||||
isMuted: false,
|
||||
volume: 1.0,
|
||||
rate: 1.0,
|
||||
|
@ -205,13 +205,12 @@ export default React.memo(function ChatInput({
|
|||
|
||||
const uploadAudio = useCallback(async () => {
|
||||
const uri = recording.getURI();
|
||||
const filetype = uri.split(".").pop();
|
||||
const fd = new FormData();
|
||||
fd.append("data[alertId]", alertId);
|
||||
fd.append("data[file]", {
|
||||
uri,
|
||||
type: `audio/${filetype}`,
|
||||
name: "audioRecord",
|
||||
type: "audio/mp4",
|
||||
name: "audioRecord.m4a",
|
||||
});
|
||||
await network.oaFilesKy.post("audio/upload", {
|
||||
body: fd,
|
||||
|
|
|
@ -24,7 +24,13 @@ export default function MessageRow({
|
|||
const { contentType, text, audioFileUuid, userId, createdAt, username } = row;
|
||||
|
||||
const audioFileUri =
|
||||
contentType === "audio" ? `${env.MINIO_URL}/audio/${audioFileUuid}` : null;
|
||||
contentType === "audio"
|
||||
? `${env.MINIO_URL}/audio/${audioFileUuid}.m4a`
|
||||
: null;
|
||||
|
||||
// if (contentType === "audio" && __DEV__) {
|
||||
// console.log(`[MessageRow] Audio URL: ${audioFileUri}`);
|
||||
// }
|
||||
|
||||
const isMine = userId === sessionUserId;
|
||||
|
||||
|
|
|
@ -17,11 +17,13 @@ import {
|
|||
import { createStyles, useTheme } from "~/theme";
|
||||
import openSettings from "~/lib/native/openSettings";
|
||||
import {
|
||||
RequestDisableOptimization,
|
||||
BatteryOptEnabled,
|
||||
} from "react-native-battery-optimization-check";
|
||||
requestBatteryOptimizationExemption,
|
||||
isBatteryOptimizationEnabled,
|
||||
openBatteryOptimizationFallbacks,
|
||||
} from "~/lib/native/batteryOptimization";
|
||||
|
||||
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
||||
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
|
||||
import requestPermissionMotion from "~/permissions/requestPermissionMotion";
|
||||
import CustomButton from "~/components/CustomButton";
|
||||
import Text from "~/components/Text";
|
||||
|
@ -45,6 +47,8 @@ const HeroMode = () => {
|
|||
useState(null);
|
||||
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
|
||||
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
|
||||
const [batteryOptFallbackOpened, setBatteryOptFallbackOpened] =
|
||||
useState(false);
|
||||
const permissions = usePermissionsState([
|
||||
"locationBackground",
|
||||
"motion",
|
||||
|
@ -75,16 +79,14 @@ const HeroMode = () => {
|
|||
setBatteryOptInProgress(true);
|
||||
|
||||
// Check if battery optimization is enabled
|
||||
const isEnabled = await BatteryOptEnabled();
|
||||
const isEnabled = await isBatteryOptimizationEnabled();
|
||||
setBatteryOptimizationEnabled(isEnabled);
|
||||
|
||||
if (isEnabled) {
|
||||
console.log(
|
||||
"Battery optimization is enabled, requesting to disable...",
|
||||
);
|
||||
console.log("Battery optimization is enabled, requesting exemption...");
|
||||
|
||||
// Request to disable battery optimization (opens Android Settings)
|
||||
RequestDisableOptimization();
|
||||
await requestBatteryOptimizationExemption();
|
||||
setBatteryOptAttempted(true);
|
||||
|
||||
// Return false to indicate user needs to complete action in Settings
|
||||
|
@ -106,31 +108,45 @@ const HeroMode = () => {
|
|||
const handleRequestPermissions = useCallback(async () => {
|
||||
setRequesting(true);
|
||||
try {
|
||||
// Don't change step immediately to avoid race conditions
|
||||
console.log("Starting permission requests...");
|
||||
|
||||
// Request battery optimization FIRST (opens Android Settings)
|
||||
// This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs
|
||||
// 1) Battery optimization (opens Settings)
|
||||
const batteryOptDisabled = await handleBatteryOptimization();
|
||||
console.log("Battery optimization disabled:", batteryOptDisabled);
|
||||
if (!batteryOptDisabled) {
|
||||
// Settings flow opened; wait for user to return before requesting in-app permissions
|
||||
setRequesting(false);
|
||||
setHasAttempted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Request motion permission second
|
||||
// 2) Foreground location
|
||||
let fgGranted = await requestPermissionLocationForeground();
|
||||
permissionsActions.setLocationForeground(fgGranted);
|
||||
console.log("Location foreground permission:", fgGranted);
|
||||
|
||||
// 3) Background location (only after FG granted)
|
||||
let bgGranted = false;
|
||||
if (fgGranted) {
|
||||
bgGranted = await requestPermissionLocationBackground();
|
||||
permissionsActions.setLocationBackground(bgGranted);
|
||||
} else {
|
||||
console.log(
|
||||
"Skipping background location since foreground not granted",
|
||||
);
|
||||
}
|
||||
console.log("Location background permission:", bgGranted);
|
||||
|
||||
// 4) Motion
|
||||
const motionGranted = await requestPermissionMotion.requestPermission();
|
||||
permissionsActions.setMotion(motionGranted);
|
||||
console.log("Motion permission:", motionGranted);
|
||||
|
||||
// Request background location last (after user returns from Settings if needed)
|
||||
const locationGranted = await requestPermissionLocationBackground();
|
||||
permissionsActions.setLocationBackground(locationGranted);
|
||||
console.log("Location background permission:", locationGranted);
|
||||
|
||||
// Only set step to tracking after all permission requests are complete
|
||||
// Step after all requests
|
||||
permissionWizardActions.setCurrentStep("tracking");
|
||||
|
||||
// Check if we should proceed to success immediately
|
||||
if (locationGranted && motionGranted && batteryOptDisabled) {
|
||||
if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) {
|
||||
permissionWizardActions.setHeroPermissionsGranted(true);
|
||||
// Don't navigate immediately, let the useEffect handle it
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error requesting permissions:", error);
|
||||
|
@ -143,7 +159,7 @@ const HeroMode = () => {
|
|||
// Re-check battery optimization status before retrying
|
||||
if (Platform.OS === "android") {
|
||||
try {
|
||||
const isEnabled = await BatteryOptEnabled();
|
||||
const isEnabled = await isBatteryOptimizationEnabled();
|
||||
setBatteryOptimizationEnabled(isEnabled);
|
||||
|
||||
// If battery optimization is now disabled, update the store
|
||||
|
@ -179,7 +195,7 @@ const HeroMode = () => {
|
|||
const checkInitialBatteryOptimization = async () => {
|
||||
if (Platform.OS === "android") {
|
||||
try {
|
||||
const isEnabled = await BatteryOptEnabled();
|
||||
const isEnabled = await isBatteryOptimizationEnabled();
|
||||
setBatteryOptimizationEnabled(isEnabled);
|
||||
|
||||
// If already disabled, update the store
|
||||
|
@ -208,7 +224,7 @@ const HeroMode = () => {
|
|||
) {
|
||||
console.log("App became active, re-checking battery optimization...");
|
||||
try {
|
||||
const isEnabled = await BatteryOptEnabled();
|
||||
const isEnabled = await isBatteryOptimizationEnabled();
|
||||
setBatteryOptimizationEnabled(isEnabled);
|
||||
|
||||
if (!isEnabled) {
|
||||
|
@ -216,6 +232,19 @@ const HeroMode = () => {
|
|||
"Battery optimization disabled after returning from settings",
|
||||
);
|
||||
permissionsActions.setBatteryOptimizationDisabled(true);
|
||||
} else if (!batteryOptFallbackOpened) {
|
||||
try {
|
||||
console.log(
|
||||
"Battery optimization still enabled; opening fallback settings...",
|
||||
);
|
||||
await openBatteryOptimizationFallbacks();
|
||||
setBatteryOptFallbackOpened(true);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error opening battery optimization fallback settings:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
@ -234,7 +263,7 @@ const HeroMode = () => {
|
|||
return () => {
|
||||
subscription?.remove();
|
||||
};
|
||||
}, [batteryOptAttempted]);
|
||||
}, [batteryOptAttempted, batteryOptFallbackOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAttempted && allGranted) {
|
||||
|
|
|
@ -110,6 +110,7 @@ class AudioSlider extends PureComponent {
|
|||
}
|
||||
|
||||
mapAudioToCurrentTime = async () => {
|
||||
if (!this.soundObject) return;
|
||||
await this.soundObject.setPositionAsync(this.state.currentTime);
|
||||
};
|
||||
|
||||
|
@ -122,6 +123,7 @@ class AudioSlider extends PureComponent {
|
|||
};
|
||||
|
||||
play = async () => {
|
||||
if (!this.soundObject) return;
|
||||
if (this.registry && this.pauseAllBeforePlay) {
|
||||
const players = this.registry.getAll();
|
||||
await Promise.all(
|
||||
|
@ -134,12 +136,14 @@ class AudioSlider extends PureComponent {
|
|||
};
|
||||
|
||||
pause = async () => {
|
||||
if (!this.soundObject) return;
|
||||
await this.soundObject.pauseAsync();
|
||||
this.setState({ playing: false }); // This is for the play-button to go to pause
|
||||
Animated.timing(this.state.dotOffset, { useNativeDriver: false }).stop(); // Will also call animationPausedOrStopped()
|
||||
};
|
||||
|
||||
startMovingDot = async () => {
|
||||
if (!this.soundObject) return;
|
||||
const status = await this.soundObject.getStatusAsync();
|
||||
const durationLeft = status["durationMillis"] - status["positionMillis"];
|
||||
|
||||
|
@ -156,6 +160,7 @@ class AudioSlider extends PureComponent {
|
|||
// Audio has been paused
|
||||
return;
|
||||
}
|
||||
if (!this.soundObject) return;
|
||||
// Animation-duration is over (reset Animation and Audio):
|
||||
await sleep(200); // In case animation has finished, but audio has not
|
||||
this.setState({ playing: false });
|
||||
|
@ -164,6 +169,16 @@ class AudioSlider extends PureComponent {
|
|||
await this.soundObject.setPositionAsync(0);
|
||||
};
|
||||
|
||||
handlePlaybackFinished = async () => {
|
||||
// console.log(`[AudioSlider] Playback finished, resetting for replay`);
|
||||
// Reset for replay instead of unloading
|
||||
this.setState({ playing: false });
|
||||
await this.state.dotOffset.setValue({ x: 0, y: 0 });
|
||||
if (this.soundObject) {
|
||||
await this.soundObject.stopAsync();
|
||||
}
|
||||
};
|
||||
|
||||
measureTrack = (event) => {
|
||||
this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height}
|
||||
};
|
||||
|
@ -171,27 +186,92 @@ class AudioSlider extends PureComponent {
|
|||
async componentDidMount() {
|
||||
// https://github.com/olapiv/expo-audio-player/issues/13
|
||||
|
||||
const loadAudio = async () => {
|
||||
try {
|
||||
const { sound: newSound } = await Audio.Sound.createAsync({
|
||||
uri: this.props.audio,
|
||||
});
|
||||
this.soundObject = newSound;
|
||||
const audioUrl = this.props.audio;
|
||||
|
||||
// // https://github.com/expo/expo/issues/1873
|
||||
const loadAudio = async () => {
|
||||
const tryLoad = async (ext) => {
|
||||
// console.log(`[AudioSlider] Attempting to load with extension: ${ext}`);
|
||||
const { sound } = await Audio.Sound.createAsync({
|
||||
uri: audioUrl,
|
||||
overrideFileExtensionAndroid: ext,
|
||||
});
|
||||
return sound;
|
||||
};
|
||||
|
||||
let lastError = null;
|
||||
|
||||
try {
|
||||
// First try with m4a (preferred)
|
||||
const sound = await tryLoad("m4a");
|
||||
// console.log(`[AudioSlider] Successfully loaded with m4a extension`);
|
||||
this.soundObject = sound;
|
||||
await this.soundObject.setIsLoopingAsync(false);
|
||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||
if (!status.didJustFinish) return;
|
||||
this.soundObject.unloadAsync().catch(() => {});
|
||||
this.handlePlaybackFinished();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error loading audio:", error);
|
||||
return;
|
||||
} catch (err1) {
|
||||
// console.log(`[AudioSlider] Failed to load with m4a:`, err1.message);
|
||||
lastError = err1;
|
||||
try {
|
||||
// Fallback to mp4
|
||||
const sound = await tryLoad("mp4");
|
||||
// console.log(`[AudioSlider] Successfully loaded with mp4 extension`);
|
||||
this.soundObject = sound;
|
||||
await this.soundObject.setIsLoopingAsync(false);
|
||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||
if (!status.didJustFinish) return;
|
||||
this.handlePlaybackFinished();
|
||||
});
|
||||
return;
|
||||
} catch (err2) {
|
||||
// console.log(`[AudioSlider] Failed to load with mp4:`, err2.message);
|
||||
lastError = err2;
|
||||
try {
|
||||
// Last fallback to aac
|
||||
const sound = await tryLoad("aac");
|
||||
// console.log(`[AudioSlider] Successfully loaded with aac extension`);
|
||||
this.soundObject = sound;
|
||||
await this.soundObject.setIsLoopingAsync(false);
|
||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||
if (!status.didJustFinish) return;
|
||||
this.handlePlaybackFinished();
|
||||
});
|
||||
return;
|
||||
} catch (err3) {
|
||||
// console.log(`[AudioSlider] Failed to load with aac:`, err3.message);
|
||||
lastError = err3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All attempts failed
|
||||
console.error(
|
||||
`[AudioSlider] All load attempts failed for ${audioUrl}. Last error:`,
|
||||
lastError,
|
||||
);
|
||||
};
|
||||
|
||||
await loadAudio();
|
||||
|
||||
const status = await this.soundObject.getStatusAsync();
|
||||
this.setState({ duration: status.durationMillis });
|
||||
if (!this.soundObject) {
|
||||
// Loading failed; avoid further calls and leave UI inert or show error
|
||||
console.log(
|
||||
`[AudioSlider] No sound object created, setting duration to 0`,
|
||||
);
|
||||
this.setState({ duration: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await this.soundObject.getStatusAsync();
|
||||
this.setState({ duration: status.durationMillis });
|
||||
} catch (error) {
|
||||
console.log("Error getting audio status:", error);
|
||||
this.setState({ duration: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
// This requires measureTrack to have been called.
|
||||
this.state.dotOffset.addListener(() => {
|
||||
|
@ -207,7 +287,9 @@ class AudioSlider extends PureComponent {
|
|||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
await this.soundObject.unloadAsync();
|
||||
if (this.soundObject) {
|
||||
await this.soundObject.unloadAsync();
|
||||
}
|
||||
this.state.dotOffset.removeAllListeners();
|
||||
if (this.registry) {
|
||||
this.registry.unregister(this);
|
||||
|
|
97
src/lib/native/batteryOptimization.js
Normal file
97
src/lib/native/batteryOptimization.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { Platform } from "react-native";
|
||||
import SendIntentAndroid from "react-native-send-intent";
|
||||
import {
|
||||
RequestDisableOptimization,
|
||||
BatteryOptEnabled,
|
||||
} from "react-native-battery-optimization-check";
|
||||
import { createLogger } from "~/lib/logger";
|
||||
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
|
||||
|
||||
const log = createLogger({
|
||||
module: FEATURE_SCOPES.PERMISSIONS,
|
||||
feature: "battery-optimization",
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns true if battery optimization is currently ENABLED for this app on Android.
|
||||
* On iOS, returns false (no battery optimization concept).
|
||||
*/
|
||||
export async function isBatteryOptimizationEnabled() {
|
||||
if (Platform.OS !== "android") return false;
|
||||
try {
|
||||
const enabled = await BatteryOptEnabled();
|
||||
log.info("Battery optimization status", { enabled });
|
||||
return enabled;
|
||||
} catch (e) {
|
||||
log.error("Failed to read battery optimization status", {
|
||||
error: e?.message,
|
||||
stack: e?.stack,
|
||||
});
|
||||
// Conservative: assume enabled if unknown
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the primary system flow to request ignoring battery optimizations.
|
||||
* This opens a Settings screen; it does not yield a synchronous result.
|
||||
*
|
||||
* Returns:
|
||||
* - false on Android to indicate the user must complete an action in Settings
|
||||
* - true on iOS (no-op)
|
||||
*/
|
||||
export async function requestBatteryOptimizationExemption() {
|
||||
if (Platform.OS !== "android") return true;
|
||||
|
||||
try {
|
||||
log.info("Requesting to disable battery optimization (primary intent)");
|
||||
// This opens the OS dialog/settings. No result is provided, handle via AppState return.
|
||||
RequestDisableOptimization();
|
||||
return false;
|
||||
} catch (e) {
|
||||
log.error("Primary request to disable battery optimization failed", {
|
||||
error: e?.message,
|
||||
stack: e?.stack,
|
||||
});
|
||||
// Even if it throws, we'll guide users via fallbacks.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens best-effort fallback screens to let users disable battery optimization.
|
||||
* Call this AFTER the user returns and status is still enabled.
|
||||
*
|
||||
* Strategy:
|
||||
* - Try the list of battery optimization exceptions
|
||||
* - Fallback to app settings
|
||||
*/
|
||||
export async function openBatteryOptimizationFallbacks() {
|
||||
if (Platform.OS !== "android") return true;
|
||||
|
||||
// Try the generic battery optimization settings list
|
||||
try {
|
||||
log.info("Opening fallback: IGNORE_BATTERY_OPTIMIZATION_SETTINGS");
|
||||
await SendIntentAndroid.openSettings(
|
||||
"android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS",
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.warn("Failed to open IGNORE_BATTERY_OPTIMIZATION_SETTINGS", {
|
||||
error: e?.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Final fallback: app details settings
|
||||
try {
|
||||
log.info("Opening fallback: APPLICATION_DETAILS_SETTINGS (app details)");
|
||||
await SendIntentAndroid.openAppSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.error("Failed to open APPLICATION_DETAILS_SETTINGS", {
|
||||
error: e?.message,
|
||||
stack: e?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Platform, Linking } from "react-native";
|
|||
import RNImmediatePhoneCall from "react-native-immediate-phone-call";
|
||||
|
||||
export function phoneCallEmergency() {
|
||||
const emergencyNumber = "+112";
|
||||
const emergencyNumber = "112";
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
// Use telprompt URL scheme on iOS
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default {
|
||||
red: "Urgence vitale.\nAppeler immétiatement les secours.",
|
||||
red: "Urgence vitale.\nAppeler immédiatement les secours.",
|
||||
yellow: "Danger sans risque vital.\nConcerne les services d'intervention.",
|
||||
green:
|
||||
"Entre-aide citoyenne localisée.\nJ'ai besoin d'une aide rapide à proximité.",
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import SectionSeparator from "./SectionSeparator";
|
||||
import menuItem from "./menuItem";
|
||||
|
||||
const index1 = 5;
|
||||
const index2 = 9;
|
||||
|
||||
export default function DrawerItemList(props) {
|
||||
const { state } = props;
|
||||
const { index } = state;
|
||||
|
@ -17,18 +20,18 @@ export default function DrawerItemList(props) {
|
|||
|
||||
const { routes } = state;
|
||||
|
||||
const section1 = routes.slice(0, 5);
|
||||
const section2 = routes.slice(5, 9);
|
||||
const section3 = routes.slice(9, routes.length);
|
||||
const section1 = routes.slice(0, index1);
|
||||
const section2 = routes.slice(index1, index2);
|
||||
const section3 = routes.slice(index2, routes.length);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionSeparator label="Mon compte" />
|
||||
{section2.map((props, i) => routeMenuItem(props, i + 5))}
|
||||
{section2.map((props, i) => routeMenuItem(props, i + index1))}
|
||||
<SectionSeparator label="Alerter" />
|
||||
{section1.map((props, i) => routeMenuItem(props, i + 0))}
|
||||
<SectionSeparator label="Infos pratiques" />
|
||||
{section3.map((props, i) => routeMenuItem(props, i + 8))}
|
||||
{section3.map((props, i) => routeMenuItem(props, i + index2))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -89,7 +89,9 @@ export default async function notifAlert(data) {
|
|||
|
||||
// Generate notification content
|
||||
const { title, body, bigText } = generateAlertContent({
|
||||
oneAlert: { alertTag, code, level },
|
||||
alertTag,
|
||||
code,
|
||||
level,
|
||||
initialDistance,
|
||||
reason,
|
||||
});
|
||||
|
|
|
@ -88,9 +88,9 @@ export const generateSuggestKeepOpenContent = (data) => {
|
|||
|
||||
export const generateBackgroundGeolocationLostContent = (data) => {
|
||||
return {
|
||||
title: `Alerte-Secours ne peut plus accéder à votre position`,
|
||||
body: `Vous ne pouvez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
||||
bigText: `Alerte-Secours ne peut plus accéder à votre position en arrière-plan. Vous ne pouvez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
||||
title: `Alerte-Secours ne reçoit plus de mises à jour de votre position`,
|
||||
body: `Vous ne pourrez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
||||
bigText: `Alerte-Secours ne reçoit plus de mises à jour de votre position en arrière-plan. Vous ne pourrez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import notifSuggestKeepOpen from "./channels/notifSuggestKeepOpen";
|
|||
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
||||
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
||||
import notifBackgroundGeolocationLost from "./channels/notifBackgroundGeolocationLost";
|
||||
import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync";
|
||||
// import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync.js.bak";
|
||||
|
||||
const displayLogger = createLogger({
|
||||
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||
|
@ -23,7 +23,7 @@ const SUPPORTED_ACTIONS = {
|
|||
"relative-allow-ask": notifRelativeAllowAsk,
|
||||
"relative-invitation": notifRelativeInvitation,
|
||||
"background-geolocation-lost": notifBackgroundGeolocationLost,
|
||||
"geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
|
||||
// "geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
|
||||
};
|
||||
|
||||
export default async function displayNotificationHandler(data) {
|
||||
|
|
|
@ -69,7 +69,10 @@ export default withConnectivity(function AlertAggListArchived() {
|
|||
value={sortBy}
|
||||
>
|
||||
<ToggleButton
|
||||
style={styles.sortByButton}
|
||||
style={[
|
||||
styles.sortByButton,
|
||||
sortBy === "createdAt" && styles.sortByButtonSelected,
|
||||
]}
|
||||
icon={() => (
|
||||
// <MaterialIcons
|
||||
// name="access-time"
|
||||
|
@ -79,18 +82,29 @@ export default withConnectivity(function AlertAggListArchived() {
|
|||
<MaterialCommunityIcons
|
||||
name="clock-time-four-outline"
|
||||
size={20}
|
||||
color={colors.surfaceSecondary}
|
||||
color={
|
||||
sortBy === "createdAt"
|
||||
? colors.onPrimary
|
||||
: colors.onSurfaceVariant
|
||||
}
|
||||
/>
|
||||
)}
|
||||
value="createdAt"
|
||||
/>
|
||||
<ToggleButton
|
||||
style={styles.sortByButton}
|
||||
style={[
|
||||
styles.sortByButton,
|
||||
sortBy === "alphabetical" && styles.sortByButtonSelected,
|
||||
]}
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name="alphabetical"
|
||||
size={20}
|
||||
color={colors.surfaceSecondary}
|
||||
color={
|
||||
sortBy === "alphabetical"
|
||||
? colors.onPrimary
|
||||
: colors.onSurfaceVariant
|
||||
}
|
||||
/>
|
||||
)}
|
||||
value="alphabetical"
|
||||
|
@ -156,6 +170,12 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
|||
sortByButton: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 6,
|
||||
},
|
||||
sortByButtonSelected: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
title: {
|
||||
fontSize: 13,
|
||||
|
|
|
@ -10,9 +10,9 @@ import { Button, Title } from "react-native-paper";
|
|||
import { usePermissionsState, permissionsActions } from "~/stores";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
RequestDisableOptimization,
|
||||
BatteryOptEnabled,
|
||||
} from "react-native-battery-optimization-check";
|
||||
requestBatteryOptimizationExemption,
|
||||
isBatteryOptimizationEnabled,
|
||||
} from "~/lib/native/batteryOptimization";
|
||||
import openSettings from "~/lib/native/openSettings";
|
||||
|
||||
import requestPermissionFcm from "~/permissions/requestPermissionFcm";
|
||||
|
@ -26,23 +26,19 @@ import * as Location from "expo-location";
|
|||
import * as Notifications from "expo-notifications";
|
||||
import * as Contacts from "expo-contacts";
|
||||
|
||||
// Battery optimization request handler
|
||||
const requestBatteryOptimizationDisable = async () => {
|
||||
if (Platform.OS !== "android") {
|
||||
return true; // iOS doesn't have battery optimization
|
||||
}
|
||||
if (Platform.OS !== "android") return true;
|
||||
|
||||
try {
|
||||
const isEnabled = await BatteryOptEnabled();
|
||||
if (isEnabled) {
|
||||
console.log("Battery optimization enabled, requesting to disable...");
|
||||
RequestDisableOptimization();
|
||||
// Return false as the user needs to interact with the system dialog
|
||||
const enabled = await isBatteryOptimizationEnabled();
|
||||
if (enabled) {
|
||||
console.log("Battery optimization enabled, requesting exemption...");
|
||||
await requestBatteryOptimizationExemption();
|
||||
// User must interact in Settings; will re-check on AppState 'active'
|
||||
return false;
|
||||
} else {
|
||||
console.log("Battery optimization already disabled");
|
||||
return true;
|
||||
}
|
||||
console.log("Battery optimization already disabled");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error handling battery optimization:", error);
|
||||
return false;
|
||||
|
@ -110,8 +106,8 @@ const checkPermissionStatus = async (permission) => {
|
|||
return true; // iOS doesn't have battery optimization
|
||||
}
|
||||
try {
|
||||
const isEnabled = await BatteryOptEnabled();
|
||||
return !isEnabled; // Return true if optimization is disabled
|
||||
const enabled = await isBatteryOptimizationEnabled();
|
||||
return !enabled; // true if optimization is disabled
|
||||
} catch (error) {
|
||||
console.error("Error checking battery optimization:", error);
|
||||
return false;
|
||||
|
@ -200,24 +196,33 @@ export default function Permissions() {
|
|||
|
||||
const handleRequestPermission = async (permission) => {
|
||||
try {
|
||||
const granted = await requestPermissions[permission]();
|
||||
setPermissions[permission](granted);
|
||||
let granted = false;
|
||||
|
||||
// For battery optimization, we need to handle the async nature differently
|
||||
if (
|
||||
permission === "batteryOptimizationDisabled" &&
|
||||
Platform.OS === "android"
|
||||
) {
|
||||
// Give a short delay for the system dialog to potentially complete
|
||||
setTimeout(async () => {
|
||||
const actualStatus = await checkPermissionStatus(permission);
|
||||
setPermissions[permission](actualStatus);
|
||||
}, 1000);
|
||||
if (permission === "locationBackground") {
|
||||
// Ensure foreground location is granted first
|
||||
const fgGranted = await checkPermissionStatus("locationForeground");
|
||||
if (!fgGranted) {
|
||||
const fgReq = await requestPermissionLocationForeground();
|
||||
setPermissions.locationForeground(fgReq);
|
||||
if (!fgReq) {
|
||||
granted = false;
|
||||
} else {
|
||||
granted = await requestPermissionLocationBackground();
|
||||
}
|
||||
} else {
|
||||
granted = await requestPermissionLocationBackground();
|
||||
}
|
||||
setPermissions.locationBackground(granted);
|
||||
} else {
|
||||
// Double-check the status to ensure UI is in sync
|
||||
const actualStatus = await checkPermissionStatus(permission);
|
||||
setPermissions[permission](actualStatus);
|
||||
granted = await requestPermissions[permission]();
|
||||
setPermissions[permission](granted);
|
||||
}
|
||||
|
||||
// Double-check the status to ensure UI is in sync.
|
||||
// For battery optimization, this immediate check may still be 'enabled';
|
||||
// we'll re-check again on AppState 'active' after returning from Settings.
|
||||
const actualStatus = await checkPermissionStatus(permission);
|
||||
setPermissions[permission](actualStatus);
|
||||
} catch (error) {
|
||||
console.error(`Error requesting ${permission} permission:`, error);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue