Compare commits

...

2 commits

Author SHA1 Message Date
a461f445c4
chore(release): 1.12.1 2025-08-10 11:49:26 +02:00
28b8b3d826
fix(voice-message): wip 2025-08-10 11:48:56 +02:00
7 changed files with 128 additions and 29 deletions

View file

@ -2,6 +2,18 @@
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. 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.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) ## [1.12.0](https://github.com/alerte-secours/as-app/compare/v1.11.17...v1.12.0) (2025-08-02)

View file

@ -83,8 +83,8 @@ android {
applicationId 'com.alertesecours' applicationId 'com.alertesecours'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 208 versionCode 209
versionName "1.12.0" versionName "1.12.1"
multiDexEnabled true multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -25,7 +25,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.12.0</string> <string>1.12.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -48,7 +48,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>208</string> <string>209</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View file

@ -1,6 +1,6 @@
{ {
"name": "alerte-secours", "name": "alerte-secours",
"version": "1.12.0", "version": "1.12.1",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem", "start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
@ -50,8 +50,8 @@
"screenshot:android": "scripts/screenshot-android.sh" "screenshot:android": "scripts/screenshot-android.sh"
}, },
"customExpoVersioning": { "customExpoVersioning": {
"versionCode": 208, "versionCode": 209,
"buildNumber": 208 "buildNumber": 209
}, },
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {

View file

@ -38,16 +38,16 @@ const recordingSettings = {
outputFormat: AndroidOutputFormat.MPEG_4, outputFormat: AndroidOutputFormat.MPEG_4,
audioEncoder: AndroidAudioEncoder.AAC, audioEncoder: AndroidAudioEncoder.AAC,
sampleRate: 44100, sampleRate: 44100,
numberOfChannels: 2, numberOfChannels: 1,
bitRate: 128000, bitRate: 64000,
}, },
ios: { ios: {
extension: ".m4a", extension: ".m4a",
outputFormat: IOSOutputFormat.MPEG4AAC, outputFormat: IOSOutputFormat.MPEG4AAC,
audioQuality: IOSAudioQuality.MAX, audioQuality: IOSAudioQuality.MAX,
sampleRate: 44100, sampleRate: 44100,
numberOfChannels: 2, numberOfChannels: 1,
bitRate: 128000, bitRate: 64000,
linearPCMBitDepth: 16, linearPCMBitDepth: 16,
linearPCMIsBigEndian: false, linearPCMIsBigEndian: false,
linearPCMIsFloat: false, linearPCMIsFloat: false,
@ -194,7 +194,7 @@ export default React.memo(function ChatInput({
staysActiveInBackground: true, staysActiveInBackground: true,
}); });
const { sound: _sound } = await recording.createNewLoadedSoundAsync({ const { sound: _sound } = await recording.createNewLoadedSoundAsync({
isLooping: true, isLooping: false,
isMuted: false, isMuted: false,
volume: 1.0, volume: 1.0,
rate: 1.0, rate: 1.0,
@ -205,13 +205,12 @@ export default React.memo(function ChatInput({
const uploadAudio = useCallback(async () => { const uploadAudio = useCallback(async () => {
const uri = recording.getURI(); const uri = recording.getURI();
const filetype = uri.split(".").pop();
const fd = new FormData(); const fd = new FormData();
fd.append("data[alertId]", alertId); fd.append("data[alertId]", alertId);
fd.append("data[file]", { fd.append("data[file]", {
uri, uri,
type: `audio/${filetype}`, type: "audio/mp4",
name: "audioRecord", name: "audioRecord.m4a",
}); });
await network.oaFilesKy.post("audio/upload", { await network.oaFilesKy.post("audio/upload", {
body: fd, body: fd,

View file

@ -24,7 +24,13 @@ export default function MessageRow({
const { contentType, text, audioFileUuid, userId, createdAt, username } = row; const { contentType, text, audioFileUuid, userId, createdAt, username } = row;
const audioFileUri = 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; const isMine = userId === sessionUserId;

View file

@ -110,6 +110,7 @@ class AudioSlider extends PureComponent {
} }
mapAudioToCurrentTime = async () => { mapAudioToCurrentTime = async () => {
if (!this.soundObject) return;
await this.soundObject.setPositionAsync(this.state.currentTime); await this.soundObject.setPositionAsync(this.state.currentTime);
}; };
@ -122,6 +123,7 @@ class AudioSlider extends PureComponent {
}; };
play = async () => { play = async () => {
if (!this.soundObject) return;
if (this.registry && this.pauseAllBeforePlay) { if (this.registry && this.pauseAllBeforePlay) {
const players = this.registry.getAll(); const players = this.registry.getAll();
await Promise.all( await Promise.all(
@ -134,12 +136,14 @@ class AudioSlider extends PureComponent {
}; };
pause = async () => { pause = async () => {
if (!this.soundObject) return;
await this.soundObject.pauseAsync(); await this.soundObject.pauseAsync();
this.setState({ playing: false }); // This is for the play-button to go to pause 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() Animated.timing(this.state.dotOffset, { useNativeDriver: false }).stop(); // Will also call animationPausedOrStopped()
}; };
startMovingDot = async () => { startMovingDot = async () => {
if (!this.soundObject) return;
const status = await this.soundObject.getStatusAsync(); const status = await this.soundObject.getStatusAsync();
const durationLeft = status["durationMillis"] - status["positionMillis"]; const durationLeft = status["durationMillis"] - status["positionMillis"];
@ -156,6 +160,7 @@ class AudioSlider extends PureComponent {
// Audio has been paused // Audio has been paused
return; return;
} }
if (!this.soundObject) return;
// Animation-duration is over (reset Animation and Audio): // Animation-duration is over (reset Animation and Audio):
await sleep(200); // In case animation has finished, but audio has not await sleep(200); // In case animation has finished, but audio has not
this.setState({ playing: false }); this.setState({ playing: false });
@ -164,6 +169,16 @@ class AudioSlider extends PureComponent {
await this.soundObject.setPositionAsync(0); 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) => { measureTrack = (event) => {
this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height} this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height}
}; };
@ -171,27 +186,92 @@ class AudioSlider extends PureComponent {
async componentDidMount() { async componentDidMount() {
// https://github.com/olapiv/expo-audio-player/issues/13 // https://github.com/olapiv/expo-audio-player/issues/13
const loadAudio = async () => { const audioUrl = this.props.audio;
try {
const { sound: newSound } = await Audio.Sound.createAsync({
uri: this.props.audio,
});
this.soundObject = newSound;
// // 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) => { this.soundObject.setOnPlaybackStatusUpdate((status) => {
if (!status.didJustFinish) return; if (!status.didJustFinish) return;
this.soundObject.unloadAsync().catch(() => {}); this.handlePlaybackFinished();
}); });
} catch (error) { return;
console.log("Error loading audio:", error); } 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(); await loadAudio();
const status = await this.soundObject.getStatusAsync(); if (!this.soundObject) {
this.setState({ duration: status.durationMillis }); // 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 requires measureTrack to have been called.
this.state.dotOffset.addListener(() => { this.state.dotOffset.addListener(() => {
@ -207,7 +287,9 @@ class AudioSlider extends PureComponent {
} }
async componentWillUnmount() { async componentWillUnmount() {
await this.soundObject.unloadAsync(); if (this.soundObject) {
await this.soundObject.unloadAsync();
}
this.state.dotOffset.removeAllListeners(); this.state.dotOffset.removeAllListeners();
if (this.registry) { if (this.registry) {
this.registry.unregister(this); this.registry.unregister(this);