mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
fix(matrix-mobile): migrate from expo-av to expo-audio for SDK 55 compatibility
expo-av was removed in Expo SDK 55, causing 'EXEventEmitter.h' not found build errors. Migrated VoiceRecorder and VoiceMessage to use expo-audio. Also changed EAS build image from "latest" (Xcode 26.2 beta) to "default". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b35b9fd76e
commit
2491440fd8
6 changed files with 67 additions and 81 deletions
|
|
@ -42,7 +42,7 @@
|
|||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
"expo-av",
|
||||
"expo-audio",
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"extends": "base",
|
||||
"autoIncrement": true,
|
||||
"ios": {
|
||||
"image": "latest"
|
||||
"image": "default"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"expo": "~55.0.5",
|
||||
"expo-av": "~16.0.8",
|
||||
"expo-audio": "~55.0.8",
|
||||
"expo-constants": "~55.0.7",
|
||||
"expo-document-picker": "~55.0.8",
|
||||
"expo-file-system": "~55.0.10",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text, Pressable, ActivityIndicator } from 'react-native';
|
||||
import { Audio } from 'expo-av';
|
||||
import { useAudioPlayer, useAudioPlayerStatus, setAudioModeAsync } from 'expo-audio';
|
||||
import { Play, Pause } from 'phosphor-react-native';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -17,51 +17,27 @@ function formatDuration(ms: number): string {
|
|||
}
|
||||
|
||||
export default function VoiceMessage({ uri, duration, isOwn }: Props) {
|
||||
const soundRef = useRef<Audio.Sound | null>(null);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [position, setPosition] = useState(0);
|
||||
const [totalDuration, setTotalDuration] = useState(duration ?? 0);
|
||||
const player = useAudioPlayer(uri);
|
||||
const status = useAudioPlayerStatus(player);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const progress = totalDuration > 0 ? position / totalDuration : 0;
|
||||
const currentTimeMs = (status.currentTime ?? 0) * 1000;
|
||||
const durationMs = (status.duration ?? 0) * 1000 || duration || 0;
|
||||
const playing = status.playing;
|
||||
const progress = durationMs > 0 ? currentTimeMs / durationMs : 0;
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (loading) return;
|
||||
const handleToggle = useCallback(async () => {
|
||||
if (!initialized) {
|
||||
await setAudioModeAsync({ playsInSilentMode: true });
|
||||
setInitialized(true);
|
||||
}
|
||||
|
||||
if (playing) {
|
||||
await soundRef.current?.pauseAsync();
|
||||
setPlaying(false);
|
||||
return;
|
||||
player.pause();
|
||||
} else {
|
||||
player.play();
|
||||
}
|
||||
|
||||
if (soundRef.current) {
|
||||
await soundRef.current.playAsync();
|
||||
setPlaying(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await Audio.setAudioModeAsync({ playsInSilentModeIOS: true });
|
||||
const { sound } = await Audio.Sound.createAsync(
|
||||
{ uri },
|
||||
{ shouldPlay: true },
|
||||
(status) => {
|
||||
if (!status.isLoaded) return;
|
||||
setPosition(status.positionMillis);
|
||||
if (status.durationMillis) setTotalDuration(status.durationMillis);
|
||||
if (status.didJustFinish) {
|
||||
setPlaying(false);
|
||||
setPosition(0);
|
||||
}
|
||||
},
|
||||
);
|
||||
soundRef.current = sound;
|
||||
setPlaying(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [player, playing, initialized]);
|
||||
|
||||
const iconColor = isOwn ? '#fff' : '#7c6bff';
|
||||
const barColor = isOwn ? 'rgba(255,255,255,0.5)' : '#2a2a2a';
|
||||
|
|
@ -71,9 +47,11 @@ export default function VoiceMessage({ uri, duration, isOwn }: Props) {
|
|||
<View className="flex-row items-center gap-3 px-3 py-2.5 min-w-[160px]">
|
||||
<Pressable
|
||||
onPress={handleToggle}
|
||||
className={({ pressed }) => `w-8 h-8 rounded-full items-center justify-center ${pressed ? 'opacity-60' : ''} ${isOwn ? 'bg-white/20' : 'bg-primary/10'}`}
|
||||
className={({ pressed }) =>
|
||||
`w-8 h-8 rounded-full items-center justify-center ${pressed ? 'opacity-60' : ''} ${isOwn ? 'bg-white/20' : 'bg-primary/10'}`
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
{status.isBuffering ? (
|
||||
<ActivityIndicator size={14} color={iconColor} />
|
||||
) : playing ? (
|
||||
<Pause size={14} color={iconColor} weight="fill" />
|
||||
|
|
@ -83,12 +61,18 @@ export default function VoiceMessage({ uri, duration, isOwn }: Props) {
|
|||
</Pressable>
|
||||
|
||||
{/* Waveform / progress bar */}
|
||||
<View className="flex-1 h-1 rounded-full overflow-hidden" style={{ backgroundColor: barColor }}>
|
||||
<View style={{ width: `${progress * 100}%`, backgroundColor: fillColor }} className="h-full rounded-full" />
|
||||
<View
|
||||
className="flex-1 h-1 rounded-full overflow-hidden"
|
||||
style={{ backgroundColor: barColor }}
|
||||
>
|
||||
<View
|
||||
style={{ width: `${progress * 100}%`, backgroundColor: fillColor }}
|
||||
className="h-full rounded-full"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text className={`text-xs tabular-nums ${isOwn ? 'text-white/70' : 'text-muted-foreground'}`}>
|
||||
{formatDuration(playing || position > 0 ? position : totalDuration)}
|
||||
{formatDuration(playing || currentTimeMs > 0 ? currentTimeMs : durationMs)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Pressable, Animated, Alert } from 'react-native';
|
||||
import { Audio } from 'expo-av';
|
||||
import { Microphone, Stop, Trash, PaperPlaneRight } from 'phosphor-react-native';
|
||||
import {
|
||||
useAudioRecorder,
|
||||
RecordingPresets,
|
||||
requestRecordingPermissionsAsync,
|
||||
setAudioModeAsync,
|
||||
} from 'expo-audio';
|
||||
import { Trash, PaperPlaneRight } from 'phosphor-react-native';
|
||||
|
||||
interface Props {
|
||||
onSend: (uri: string, durationMs: number) => Promise<void>;
|
||||
|
|
@ -9,7 +14,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function VoiceRecorder({ onSend, onCancel }: Props) {
|
||||
const recordingRef = useRef<Audio.Recording | null>(null);
|
||||
const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [sending, setSending] = useState(false);
|
||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
|
|
@ -17,12 +22,11 @@ export default function VoiceRecorder({ onSend, onCancel }: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
startRecording();
|
||||
// Pulse animation
|
||||
const pulse = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, { toValue: 1.3, duration: 600, useNativeDriver: true }),
|
||||
Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
|
||||
]),
|
||||
])
|
||||
);
|
||||
pulse.start();
|
||||
return () => {
|
||||
|
|
@ -33,17 +37,15 @@ export default function VoiceRecorder({ onSend, onCancel }: Props) {
|
|||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const { status } = await Audio.requestPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
const { granted } = await requestRecordingPermissionsAsync();
|
||||
if (!granted) {
|
||||
Alert.alert('Permission required', 'Microphone access is needed to record voice messages.');
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
|
||||
const { recording } = await Audio.Recording.createAsync(
|
||||
Audio.RecordingOptionsPresets.HIGH_QUALITY,
|
||||
);
|
||||
recordingRef.current = recording;
|
||||
await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
|
||||
await recorder.prepareToRecordAsync();
|
||||
recorder.record();
|
||||
timerRef.current = setInterval(() => setDuration((d) => d + 1), 1000);
|
||||
} catch (err) {
|
||||
Alert.alert('Error', 'Could not start recording');
|
||||
|
|
@ -54,17 +56,19 @@ export default function VoiceRecorder({ onSend, onCancel }: Props) {
|
|||
const stopRecordingCleanup = async () => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
try {
|
||||
await recordingRef.current?.stopAndUnloadAsync();
|
||||
} catch { /* ignore */ }
|
||||
await recorder.stop();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!recordingRef.current || sending) return;
|
||||
if (sending) return;
|
||||
setSending(true);
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
try {
|
||||
await recordingRef.current.stopAndUnloadAsync();
|
||||
const uri = recordingRef.current.getURI();
|
||||
await recorder.stop();
|
||||
const uri = recorder.uri;
|
||||
if (!uri) throw new Error('No recording URI');
|
||||
await onSend(uri, duration * 1000);
|
||||
} catch (err) {
|
||||
|
|
@ -90,7 +94,9 @@ export default function VoiceRecorder({ onSend, onCancel }: Props) {
|
|||
{/* Discard */}
|
||||
<Pressable
|
||||
onPress={handleDiscard}
|
||||
className={({ pressed }) => `w-10 h-10 rounded-full bg-destructive/10 items-center justify-center ${pressed ? 'opacity-60' : ''}`}
|
||||
className={({ pressed }) =>
|
||||
`w-10 h-10 rounded-full bg-destructive/10 items-center justify-center ${pressed ? 'opacity-60' : ''}`
|
||||
}
|
||||
>
|
||||
<Trash size={18} color="#ef4444" />
|
||||
</Pressable>
|
||||
|
|
|
|||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
|
|
@ -2449,9 +2449,9 @@ importers:
|
|||
expo:
|
||||
specifier: ~55.0.5
|
||||
version: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-av:
|
||||
specifier: ~16.0.8
|
||||
version: 16.0.8(expo@55.0.5)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
expo-audio:
|
||||
specifier: ~55.0.8
|
||||
version: 55.0.8(expo-asset@55.0.8(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
expo-constants:
|
||||
specifier: ~55.0.7
|
||||
version: 55.0.7(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(typescript@5.9.3)
|
||||
|
|
@ -17578,16 +17578,13 @@ packages:
|
|||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-av@16.0.8:
|
||||
resolution: {integrity: sha512-cmVPftGR/ca7XBgs7R6ky36lF3OC0/MM/lpgX/yXqfv0jASTsh7AYX9JxHCwFmF+Z6JEB1vne9FDx4GiLcGreQ==}
|
||||
expo-audio@55.0.8:
|
||||
resolution: {integrity: sha512-X61pQSikE2rsP2ZTMFUMThOmgGyYEHcmZpGVMrKJgcYtRCFKuctB/z69dFQPoumL+zTz8qlBoGohjkHVvA9P8A==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
expo-asset: '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
react-native-web: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-web:
|
||||
optional: true
|
||||
|
||||
expo-blur@14.0.3:
|
||||
resolution: {integrity: sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==}
|
||||
|
|
@ -42423,13 +42420,12 @@ snapshots:
|
|||
- typescript
|
||||
optional: true
|
||||
|
||||
expo-av@16.0.8(expo@55.0.5)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
expo-audio@55.0.8(expo-asset@55.0.8(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-asset: 55.0.8(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
|
||||
expo-blur@14.0.3(expo@52.0.47)(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue