import React, { useContext, useEffect, useRef, useState } from 'react'; import { StyleProp, StyleSheet, Text, TextStyle, View, useWindowDimensions } from 'react-native'; import { Audio, AVPlaybackStatus, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av'; import Slider from '@react-native-community/slider'; import moment from 'moment'; import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'; import { Sound } from 'expo-av/build/Audio/Sound'; import Touchable from './Touchable'; import Markdown from '../markdown'; import { CustomIcon } from '../CustomIcon'; import sharedStyles from '../../views/Styles'; import { themes } from '../../lib/constants'; import { isAndroid, isIOS } from '../../lib/methods/helpers'; import MessageContext from './Context'; import ActivityIndicator from '../ActivityIndicator'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { IAttachment, IUserMessage } from '../../definitions'; import { useTheme } from '../../theme'; import { downloadMediaFile, getMediaCache } from '../../lib/methods/handleMediaDownload'; import EventEmitter from '../../lib/methods/helpers/events'; import { PAUSE_AUDIO } from './constants'; import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference'; interface IButton { loading: boolean; paused: boolean; disabled?: boolean; onPress: () => void; cached: boolean; } interface IMessageAudioProps { file: IAttachment; isReply?: boolean; style?: StyleProp[]; getCustomEmoji: TGetCustomEmoji; author?: IUserMessage; } const mode = { allowsRecordingIOS: false, playsInSilentModeIOS: true, staysActiveInBackground: true, shouldDuckAndroid: true, playThroughEarpieceAndroid: false, interruptionModeIOS: InterruptionModeIOS.DoNotMix, interruptionModeAndroid: InterruptionModeAndroid.DoNotMix }; const styles = StyleSheet.create({ audioContainer: { flex: 1, flexDirection: 'row', alignItems: 'center', height: 56, borderWidth: 1, borderRadius: 4, marginBottom: 6 }, playPauseButton: { marginHorizontal: 10, alignItems: 'center', backgroundColor: 'transparent' }, audioLoading: { marginHorizontal: 8 }, slider: { flex: 1 }, duration: { marginHorizontal: 12, fontSize: 14, ...sharedStyles.textRegular } }); const formatTime = (seconds: number) => moment.utc(seconds * 1000).format('mm:ss'); const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 }; const Button = React.memo(({ loading, paused, onPress, disabled, cached }: IButton) => { const { colors } = useTheme(); let customIconName: 'arrow-down-circle' | 'play-filled' | 'pause-filled' = 'arrow-down-circle'; if (cached) { customIconName = paused ? 'play-filled' : 'pause-filled'; } return ( {loading ? ( ) : ( )} ); }); Button.displayName = 'MessageAudioButton'; const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessageAudioProps) => { const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [paused, setPaused] = useState(true); const [cached, setCached] = useState(false); const { baseUrl, user } = useContext(MessageContext); const { scale } = useWindowDimensions(); const { theme } = useTheme(); const sound = useRef(null); const pauseSound = () => { EventEmitter.removeListener(PAUSE_AUDIO, pauseSound); togglePlayPause(); }; const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => { if (status) { onLoad(status); onProgress(status); onEnd(status); } }; const getUrl = () => { let url = file.audio_url; if (url && !url.startsWith('http')) { url = `${baseUrl}${file.audio_url}`; } return url; }; const onLoad = (data: AVPlaybackStatus) => { if (data.isLoaded && data.durationMillis) { const duration = data.durationMillis / 1000; setDuration(duration > 0 ? duration : 0); } }; const onProgress = (data: AVPlaybackStatus) => { if (data.isLoaded) { const currentTime = data.positionMillis / 1000; if (currentTime <= duration) { setCurrentTime(currentTime); } } }; const onEnd = async (data: AVPlaybackStatus) => { if (data.isLoaded) { if (data.didJustFinish) { try { await sound.current?.stopAsync(); setPaused(true); setCurrentTime(0); EventEmitter.removeListener(PAUSE_AUDIO, pauseSound); } catch { // do nothing } } } }; const getDuration = () => formatTime(currentTime || duration); const togglePlayPause = () => { setPaused(!paused); playPause(!paused); }; const playPause = async (isPaused: boolean) => { try { if (isPaused) { await sound.current?.pauseAsync(); EventEmitter.removeListener(PAUSE_AUDIO, pauseSound); } else { EventEmitter.emit(PAUSE_AUDIO); EventEmitter.addEventListener(PAUSE_AUDIO, pauseSound); await Audio.setAudioModeAsync(mode); await sound.current?.playAsync(); } } catch { // Do nothing } }; const handleDownload = async () => { setLoading(true); try { const url = getUrl(); if (url) { const audio = await downloadMediaFile({ downloadUrl: `${url}?rc_uid=${user.id}&rc_token=${user.token}`, type: 'audio', mimeType: file.audio_type }); await sound.current?.loadAsync({ uri: audio }); setLoading(false); setCached(true); } } catch { setLoading(false); setCached(false); } }; const handleAutoDownload = async () => { const url = getUrl(); try { if (url) { const isCurrentUserAuthor = author?._id === user.id; const isAutoDownloadEnabled = fetchAutoDownloadEnabled('audioPreferenceDownload'); if (isAutoDownloadEnabled || isCurrentUserAuthor) { await handleDownload(); return; } setLoading(false); setCached(false); } } catch { // Do nothing } }; const onPress = () => { if (cached) { togglePlayPause(); return; } handleDownload(); }; const onValueChange = async (value: number) => { try { setCurrentTime(value); await sound.current?.setPositionAsync(value * 1000); } catch { // Do nothing } }; useEffect(() => { sound.current = new Audio.Sound(); sound.current?.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); const handleCache = async () => { const cachedAudioResult = await getMediaCache({ type: 'audio', mimeType: file.audio_type, urlToCache: getUrl() }); if (cachedAudioResult?.exists) { await sound.current?.loadAsync({ uri: cachedAudioResult.uri }); setLoading(false); setCached(true); return; } if (isReply) { setLoading(false); return; } await handleAutoDownload(); }; handleCache(); return () => { EventEmitter.removeListener(PAUSE_AUDIO, pauseSound); try { sound.current?.stopAsync(); } catch { // Do nothing } }; }, []); useEffect(() => { if (paused) { deactivateKeepAwake(); } else { activateKeepAwakeAsync(); } }, [paused]); if (!baseUrl) { return null; } let thumbColor; if (isAndroid && isReply) { thumbColor = themes[theme].tintDisabled; } else if (isAndroid) { thumbColor = themes[theme].tintColor; } return ( <>