diff --git a/app/containers/message/Components/Audio/Loading.tsx b/app/containers/AudioPlayer/Loading.tsx similarity index 87% rename from app/containers/message/Components/Audio/Loading.tsx rename to app/containers/AudioPlayer/Loading.tsx index 34baf68f2..90acf2c53 100644 --- a/app/containers/message/Components/Audio/Loading.tsx +++ b/app/containers/AudioPlayer/Loading.tsx @@ -1,8 +1,8 @@ import React, { useEffect } from 'react'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; -import { CustomIcon } from '../../../CustomIcon'; -import { useTheme } from '../../../../theme'; +import { CustomIcon } from '../CustomIcon'; +import { useTheme } from '../../theme'; const Loading = () => { const rotation = useSharedValue(0); diff --git a/app/containers/message/Components/Audio/PlayButton.tsx b/app/containers/AudioPlayer/PlayButton.tsx similarity index 89% rename from app/containers/message/Components/Audio/PlayButton.tsx rename to app/containers/AudioPlayer/PlayButton.tsx index 913e81e2d..80501596e 100644 --- a/app/containers/message/Components/Audio/PlayButton.tsx +++ b/app/containers/AudioPlayer/PlayButton.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import Touchable from '../../Touchable'; -import { CustomIcon } from '../../../CustomIcon'; -import { useTheme } from '../../../../theme'; +import Touchable from '../message/Touchable'; +import { CustomIcon } from '../CustomIcon'; +import { useTheme } from '../../theme'; import styles from './styles'; import Loading from './Loading'; diff --git a/app/containers/message/Components/Audio/AudioRate.tsx b/app/containers/AudioPlayer/PlaybackSpeed.tsx similarity index 56% rename from app/containers/message/Components/Audio/AudioRate.tsx rename to app/containers/AudioPlayer/PlaybackSpeed.tsx index 2fddb1d55..0d14ed934 100644 --- a/app/containers/message/Components/Audio/AudioRate.tsx +++ b/app/containers/AudioPlayer/PlaybackSpeed.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { Text } from 'react-native'; import styles from './styles'; -import { useTheme } from '../../../../theme'; -import Touchable from '../../Touchable'; +import { useTheme } from '../../theme'; +import Touchable from '../message/Touchable'; -const AudioRate = ({ +const PlaybackSpeed = ({ onChange, loaded = false, rate = 1 @@ -25,11 +25,11 @@ const AudioRate = ({ - {rate}x + {rate}x ); }; -export default AudioRate; +export default PlaybackSpeed; diff --git a/app/containers/message/Components/Audio/Slider.tsx b/app/containers/AudioPlayer/Seek.tsx similarity index 90% rename from app/containers/message/Components/Audio/Slider.tsx rename to app/containers/AudioPlayer/Seek.tsx index 6f8977ce1..30babadba 100644 --- a/app/containers/message/Components/Audio/Slider.tsx +++ b/app/containers/AudioPlayer/Seek.tsx @@ -12,11 +12,11 @@ import Animated, { } from 'react-native-reanimated'; import styles from './styles'; -import { useTheme } from '../../../../theme'; +import { useTheme } from '../../theme'; const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -interface ISlider { +interface ISeek { duration: SharedValue; currentTime: SharedValue; loaded: boolean; @@ -30,7 +30,7 @@ const BUTTON_HIT_SLOP = { left: 8 }; -const Slider = ({ currentTime, duration, loaded = false, onChangeTime }: ISlider) => { +const Seek = ({ currentTime, duration, loaded = false, onChangeTime }: ISeek) => { const { colors } = useTheme(); const maxWidth = useSharedValue(1); @@ -90,6 +90,7 @@ const Slider = ({ currentTime, duration, loaded = false, onChangeTime }: ISlider } }); + // https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/ const formatTime = (ms: number) => { 'worklet'; @@ -129,22 +130,22 @@ const Slider = ({ currentTime, duration, loaded = false, onChangeTime }: ISlider const thumbColor = loaded ? colors.buttonBackgroundPrimaryDefault : colors.tintDisabled; return ( - + - + - + ); }; -export default Slider; +export default Seek; diff --git a/app/containers/AudioPlayer/index.tsx b/app/containers/AudioPlayer/index.tsx new file mode 100644 index 000000000..459dfff39 --- /dev/null +++ b/app/containers/AudioPlayer/index.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { StyleProp, TextStyle, View } from 'react-native'; +import { AVPlaybackStatus } from 'expo-av'; +import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; +import { useSharedValue } from 'react-native-reanimated'; + +import { IAttachment, IUserMessage } from '../../definitions'; +import { useTheme } from '../../theme'; +import { downloadMediaFile, getMediaCache } from '../../lib/methods/handleMediaDownload'; +import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference'; +import styles from './styles'; +import Seek from './Seek'; +import PlaybackSpeed from './PlaybackSpeed'; +import PlayButton from './PlayButton'; +import audioPlayer from '../../lib/methods/audioPlayer'; + +interface IAudioPlayerProps { + file: IAttachment; + isReply?: boolean; + style?: StyleProp[]; + author?: IUserMessage; + msg?: string; + baseUrl: string; + user: any; +} + +const AudioPlayer = ({ file, author, isReply = false, baseUrl, user }: IAudioPlayerProps) => { + const [loading, setLoading] = useState(true); + const [paused, setPaused] = useState(true); + const [cached, setCached] = useState(false); + const [rate, setRate] = useState(1); + + const duration = useSharedValue(0); + const currentTime = useSharedValue(0); + + const { colors } = useTheme(); + + const audioUri = useRef(''); + + const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => { + if (status) { + onPlaying(status); + handlePlaybackStatusUpdate(status); + onEnd(status); + } + }; + + const loadAudio = async (audio: string) => { + await audioPlayer.loadAudio(audio); + audioUri.current = audio; + audioPlayer.setOnPlaybackStatusUpdate(audio, onPlaybackStatusUpdate); + }; + + const onPlaying = (data: AVPlaybackStatus) => { + if (data.isLoaded && data.isPlaying) { + setPaused(false); + } else { + setPaused(true); + } + }; + + const handlePlaybackStatusUpdate = (data: AVPlaybackStatus) => { + if (data.isLoaded && data.durationMillis) { + const durationSeconds = data.durationMillis / 1000; + duration.value = durationSeconds > 0 ? durationSeconds : 0; + const currentSecond = data.positionMillis / 1000; + if (currentSecond <= durationSeconds) { + currentTime.value = currentSecond; + } + setRate(data.rate); + } + }; + + const onEnd = (data: AVPlaybackStatus) => { + if (data.isLoaded) { + if (data.didJustFinish) { + try { + setPaused(true); + currentTime.value = 0; + } catch { + // do nothing + } + } + } + }; + + const setPosition = async (time: number) => { + await audioPlayer.setPositionAsync(audioUri.current, time); + }; + + const getUrl = () => { + let url = file.audio_url; + if (url && !url.startsWith('http')) { + url = `${baseUrl}${file.audio_url}`; + } + return url; + }; + + const togglePlayPause = async () => { + try { + if (!paused) { + await audioPlayer.pauseAudio(audioUri.current); + } else { + await audioPlayer.playAudio(audioUri.current); + } + } catch { + // Do nothing + } + }; + + const onChangeRate = async (value = 1.0) => { + await audioPlayer.setRateAsync(audioUri.current, value); + }; + + 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 loadAudio(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 (loading) { + return; + } + if (cached) { + togglePlayPause(); + return; + } + handleDownload(); + }; + + useEffect(() => { + const handleCache = async () => { + const cachedAudioResult = await getMediaCache({ + type: 'audio', + mimeType: file.audio_type, + urlToCache: getUrl() + }); + if (cachedAudioResult?.exists) { + await loadAudio(cachedAudioResult.uri); + setLoading(false); + setCached(true); + return; + } + if (isReply) { + setLoading(false); + return; + } + await handleAutoDownload(); + }; + handleCache(); + }, []); + + useEffect(() => { + if (paused) { + deactivateKeepAwake(); + } else { + activateKeepAwake(); + } + }, [paused]); + + if (!baseUrl) { + return null; + } + return ( + <> + + + + + + + ); +}; + +export default AudioPlayer; diff --git a/app/containers/message/Components/Audio/styles.ts b/app/containers/AudioPlayer/styles.ts similarity index 87% rename from app/containers/message/Components/Audio/styles.ts rename to app/containers/AudioPlayer/styles.ts index 5fbbf934e..c85ec1809 100644 --- a/app/containers/message/Components/Audio/styles.ts +++ b/app/containers/AudioPlayer/styles.ts @@ -1,6 +1,6 @@ import { StyleSheet } from 'react-native'; -import sharedStyles from '../../../../views/Styles'; +import sharedStyles from '../../views/Styles'; const styles = StyleSheet.create({ audioContainer: { @@ -20,13 +20,13 @@ const styles = StyleSheet.create({ borderRadius: 4, justifyContent: 'center' }, - sliderContainer: { + seekContainer: { flexDirection: 'row', flex: 1, alignItems: 'center', height: '100%' }, - slider: { + seek: { marginRight: 12, height: '100%', justifyContent: 'center', @@ -45,13 +45,13 @@ const styles = StyleSheet.create({ fontSize: 14, ...sharedStyles.textRegular }, - thumbSlider: { + thumbSeek: { height: 12, width: 12, borderRadius: 6, zIndex: 3 }, - containerAudioRate: { + containerPlaybackSpeed: { width: 36, height: 24, borderRadius: 4, @@ -60,7 +60,7 @@ const styles = StyleSheet.create({ alignItems: 'center', overflow: 'hidden' }, - audioRateText: { + playbackSpeedText: { fontSize: 14, ...sharedStyles.textBold } diff --git a/app/containers/message/Components/Audio/index.tsx b/app/containers/message/Components/Audio/index.tsx index 50ed3e586..5425105f8 100644 --- a/app/containers/message/Components/Audio/index.tsx +++ b/app/containers/message/Components/Audio/index.tsx @@ -1,21 +1,11 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { StyleProp, TextStyle, View } from 'react-native'; -import { AVPlaybackStatus } from 'expo-av'; -import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; -import { useSharedValue } from 'react-native-reanimated'; +import React, { useContext } from 'react'; +import { StyleProp, TextStyle } from 'react-native'; import Markdown from '../../../markdown'; import MessageContext from '../../Context'; import { TGetCustomEmoji } from '../../../../definitions/IEmoji'; import { IAttachment, IUserMessage } from '../../../../definitions'; -import { useTheme } from '../../../../theme'; -import { downloadMediaFile, getMediaCache } from '../../../../lib/methods/handleMediaDownload'; -import { fetchAutoDownloadEnabled } from '../../../../lib/methods/autoDownloadPreference'; -import styles from './styles'; -import Slider from './Slider'; -import AudioRate from './AudioRate'; -import PlayButton from './PlayButton'; -import audioPlayer from '../../../../lib/methods/audioPlayer'; +import AudioPlayer from '../../../AudioPlayer'; interface IMessageAudioProps { file: IAttachment; @@ -27,172 +17,7 @@ interface IMessageAudioProps { } const MessageAudio = ({ file, getCustomEmoji, author, isReply, style, msg }: IMessageAudioProps) => { - const [loading, setLoading] = useState(true); - const [paused, setPaused] = useState(true); - const [cached, setCached] = useState(false); - const [rate, setRate] = useState(1); - - const duration = useSharedValue(0); - const currentTime = useSharedValue(0); - const { baseUrl, user } = useContext(MessageContext); - const { colors } = useTheme(); - - const audioUri = useRef(''); - - const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => { - if (status) { - onPlaying(status); - handlePlaybackStatusUpdate(status); - onEnd(status); - } - }; - - const loadAudio = async (audio: string) => { - await audioPlayer.loadAudio(audio); - audioUri.current = audio; - audioPlayer.setOnPlaybackStatusUpdate(audio, onPlaybackStatusUpdate); - }; - - const onPlaying = (data: AVPlaybackStatus) => { - if (data.isLoaded && data.isPlaying) { - setPaused(false); - } else { - setPaused(true); - } - }; - - const handlePlaybackStatusUpdate = (data: AVPlaybackStatus) => { - if (data.isLoaded && data.durationMillis) { - const durationSeconds = data.durationMillis / 1000; - duration.value = durationSeconds > 0 ? durationSeconds : 0; - const currentSecond = data.positionMillis / 1000; - if (currentSecond <= durationSeconds) { - currentTime.value = currentSecond; - } - setRate(data.rate); - } - }; - - const onEnd = (data: AVPlaybackStatus) => { - if (data.isLoaded) { - if (data.didJustFinish) { - try { - setPaused(true); - currentTime.value = 0; - } catch { - // do nothing - } - } - } - }; - - const setPosition = async (time: number) => { - await audioPlayer.setPositionAsync(audioUri.current, time); - }; - - const getUrl = () => { - let url = file.audio_url; - if (url && !url.startsWith('http')) { - url = `${baseUrl}${file.audio_url}`; - } - return url; - }; - - const togglePlayPause = async () => { - try { - if (!paused) { - await audioPlayer.pauseAudio(audioUri.current); - } else { - await audioPlayer.playAudio(audioUri.current); - } - } catch { - // Do nothing - } - }; - - const onChangeRate = async (value = 1.0) => { - await audioPlayer.setRateAsync(audioUri.current, value); - }; - - 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 loadAudio(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 (loading) { - return; - } - if (cached) { - togglePlayPause(); - return; - } - handleDownload(); - }; - - useEffect(() => { - const handleCache = async () => { - const cachedAudioResult = await getMediaCache({ - type: 'audio', - mimeType: file.audio_type, - urlToCache: getUrl() - }); - if (cachedAudioResult?.exists) { - await loadAudio(cachedAudioResult.uri); - setLoading(false); - setCached(true); - return; - } - if (isReply) { - setLoading(false); - return; - } - await handleAutoDownload(); - }; - handleCache(); - }, []); - - useEffect(() => { - if (paused) { - deactivateKeepAwake(); - } else { - activateKeepAwake(); - } - }, [paused]); if (!baseUrl) { return null; @@ -200,11 +25,7 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style, msg }: IMe return ( <> - - - - - + ); };