Add onSlidingStart and onSLidingEnd callbacks

This commit is contained in:
Danish Ahmed Mirza 2022-07-24 18:38:48 +05:30 committed by Danish
parent 0f8920203d
commit 4cb6674eb4
6 changed files with 165 additions and 152 deletions

View File

@ -10,7 +10,9 @@ import { useTheme } from '../../../../theme';
interface ISliderProps { interface ISliderProps {
value: number; value: number;
maximumValue: number; maximumValue: number;
onValueChange: (value: number) => Promise<void>; onValueChange: (value: number) => void;
onSlidingStart: () => void;
onSlidingEnd: (value: number) => void;
thumbTintColor?: string; thumbTintColor?: string;
minimumTrackTintColor?: string; minimumTrackTintColor?: string;
maximumTrackTintColor?: string; maximumTrackTintColor?: string;
@ -25,6 +27,8 @@ const Slider = React.memo(
value, value,
maximumValue, maximumValue,
onValueChange, onValueChange,
onSlidingStart,
onSlidingEnd,
thumbTintColor, thumbTintColor,
minimumTrackTintColor, minimumTrackTintColor,
maximumTrackTintColor, maximumTrackTintColor,
@ -68,6 +72,7 @@ const Slider = React.memo(
.hitSlop({ horizontal: 5, vertical: 20 }) .hitSlop({ horizontal: 5, vertical: 20 })
.onStart(() => { .onStart(() => {
setSliding(true); setSliding(true);
onSlidingStart();
sliderThumbWidth.value = withTiming(3 * SLIDER_THUMB_RADIUS, { duration: 100 }); sliderThumbWidth.value = withTiming(3 * SLIDER_THUMB_RADIUS, { duration: 100 });
}) })
.onChange(e => { .onChange(e => {
@ -78,6 +83,7 @@ const Slider = React.memo(
sliderThumbWidth.value = withTiming(2 * SLIDER_THUMB_RADIUS, { duration: 100 }); sliderThumbWidth.value = withTiming(2 * SLIDER_THUMB_RADIUS, { duration: 100 });
currentValue.value = equivalentValue(clamp(e.x, 0, sliderWidth)); currentValue.value = equivalentValue(clamp(e.x, 0, sliderWidth));
onValueChange(equivalentValue(clamp(e.x, 0, sliderWidth))); onValueChange(equivalentValue(clamp(e.x, 0, sliderWidth)));
onSlidingEnd(equivalentValue(clamp(e.x, 0, sliderWidth)));
setSliding(false); setSliding(false);
}); });

View File

@ -2,10 +2,9 @@ import React, { useContext, useEffect, useState } from 'react';
import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
// import Slider from '@react-native-community/slider'; import { Audio, AVPlaybackStatus } from 'expo-av';
import TrackPlayer, { Event, useTrackPlayerEvents, State, useProgress } from 'react-native-track-player'; import TrackPlayer, { Event, useTrackPlayerEvents, State, useProgress } from 'react-native-track-player';
import { Easing } from 'react-native-reanimated'; import { Easing } from 'react-native-reanimated';
import { useMMKVStorage } from 'react-native-mmkv-storage';
import Touchable from '../../Touchable'; import Touchable from '../../Touchable';
import Markdown from '../../../markdown'; import Markdown from '../../../markdown';
@ -23,7 +22,7 @@ import { IAttachment } from '../../../../definitions';
import { TSupportedThemes } from '../../../../theme'; import { TSupportedThemes } from '../../../../theme';
import { setupService } from './services'; import { setupService } from './services';
import Slider from './Slider'; import Slider from './Slider';
import { TracksStorage, addTrack, clearTracks, getCurrentTrack, setCurrentTrack } from './tracks'; import { useTracks } from './tracksStorage';
interface IButton { interface IButton {
loading: boolean; loading: boolean;
@ -103,15 +102,16 @@ const MessageAudio = ({
getCustomEmoji getCustomEmoji
}: // scale }: // scale
IMessageAudioProps) => { IMessageAudioProps) => {
const [loading, setLoading] = useState(false);
const [paused, setPaused] = useState(true);
const [currentTime, setCurrentTime] = useState(0);
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const { duration } = useProgress(); const [loading, setLoading] = useState(false);
const [paused, setPaused] = useState(true);
const [currentPosition, setCurrentPosition] = useState(0);
const [duration, setDuration] = useState(0);
const [isSliding, setIsSliding] = useState(false);
const { position } = useProgress();
const [tracks] = useMMKVStorage('tracks', TracksStorage); const [currentTrackId, setCurrentTrackId] = useTracks('currentTrackId');
let url = file.audio_url; let url = file.audio_url;
if (url && !url.startsWith('http')) { if (url && !url.startsWith('http')) {
@ -122,34 +122,48 @@ IMessageAudioProps) => {
id: `${url}?rc_uid=${user.id}&rc_token=${user.token}`, id: `${url}?rc_uid=${user.id}&rc_token=${user.token}`,
url: `${url}?rc_uid=${user.id}&rc_token=${user.token}`, url: `${url}?rc_uid=${user.id}&rc_token=${user.token}`,
title: file.title, title: file.title,
artist: file.author_name, artist: file.author_name
duration };
const updateTrackDuration = (status: AVPlaybackStatus) => {
if (status.isLoaded && status.durationMillis) {
const trackDuration = status.durationMillis / 1000;
setDuration(trackDuration > 0 ? trackDuration : 0);
}
}; };
useEffect(() => { useEffect(() => {
const setup = async () => { const setup = async () => {
setLoading(true); setLoading(true);
try {
await setupService(); await setupService();
addTrack({ trackId: track.id, title: file.title, artist: file.author_name, isPlaying: false }); const sound = new Audio.Sound();
sound.setOnPlaybackStatusUpdate(updateTrackDuration);
await sound.loadAsync({ uri: `${url}?rc_uid=${user.id}&rc_token=${user.token}` });
} catch {
// Do nothing
}
setLoading(false); setLoading(false);
}; };
setup(); setup();
return () => { return () => {
TrackPlayer.reset(); TrackPlayer.destroy();
clearTracks(); setCurrentTrackId(null);
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
const currentTrack = getCurrentTrack(); if (currentTrackId && currentTrackId !== track.id) {
if (currentTrack && currentTrack.trackId !== track.id) { setCurrentPosition(0);
setPaused(true); setPaused(true);
} }
}, [tracks]); }, [currentTrackId]);
// useEffect(() => { useEffect(() => {
// setCurrentTime(position); if (currentTrackId === track.id && !isSliding) {
// }, [position]); setCurrentPosition(position);
}
}, [position]);
useEffect(() => { useEffect(() => {
playPause(); playPause();
@ -161,7 +175,7 @@ IMessageAudioProps) => {
} else { } else {
activateKeepAwake(); activateKeepAwake();
} }
}, [paused, currentTime, duration, file, loading, theme]); }, [paused, currentPosition, duration, file, loading, theme]);
useTrackPlayerEvents([Event.PlaybackState], ({ state }) => { useTrackPlayerEvents([Event.PlaybackState], ({ state }) => {
if (state === State.Stopped) { if (state === State.Stopped) {
@ -172,51 +186,72 @@ IMessageAudioProps) => {
// do nothing // do nothing
} }
} }
if (state === State.Paused) {
setPaused(true);
}
if (state === State.Playing && currentTrackId?.trackId === track.id) {
setPaused(false);
}
}); });
const getDuration = () => formatTime(currentTime || duration); const getDuration = () => formatTime(currentPosition || duration);
const togglePlayPause = () => { const togglePlayPause = () => {
setPaused(!paused); setPaused(!paused);
}; };
const playPause = () => { const playPause = async () => {
const currentPlaying = getCurrentTrack();
try { try {
if (paused) { if (paused) {
if (currentPlaying?.trackId === track.id) { if (currentTrackId === track.id) {
TrackPlayer.pause(); TrackPlayer.pause();
} }
} else if (currentPlaying?.trackId === track.id) { } else if (currentTrackId === track.id) {
TrackPlayer.play(); TrackPlayer.play();
} else { } else {
TrackPlayer.reset(); TrackPlayer.reset();
TrackPlayer.add(track); await TrackPlayer.add(track);
TrackPlayer.play(); TrackPlayer.play();
setCurrentTrack(track.id); setCurrentTrackId(track.id);
} }
} catch { } catch {
// Do nothing // Do nothing
} }
}; };
const onValueChange = async (value: number) => { const onValueChange = (value: number) => {
const currentTrack = getCurrentTrack(); setCurrentPosition(value);
try { try {
setCurrentTime(value); if (currentTrackId === track.id && !paused && isSliding) {
if (currentTrack && currentTrack.trackId === track.id) { setPaused(true);
await TrackPlayer.seekTo(value); TrackPlayer.pause();
} else {
TrackPlayer.reset();
TrackPlayer.add(track);
TrackPlayer.seekTo(value);
setCurrentTrack(track.id);
} }
} catch { } catch {
// Do nothing // Do nothing
} }
}; };
const onSlidingEnd = async (value: number) => {
setCurrentPosition(value);
try {
if (currentTrackId === track.id) {
await TrackPlayer.seekTo(value);
} else {
TrackPlayer.reset();
await TrackPlayer.add(track);
await TrackPlayer.seekTo(value);
setCurrentTrackId(track.id);
}
if (paused) {
TrackPlayer.play();
setPaused(false);
}
} catch {
// Do nothing
}
setIsSliding(false);
};
const { description } = file; const { description } = file;
if (!baseUrl) { if (!baseUrl) {
@ -252,7 +287,7 @@ IMessageAudioProps) => {
]}> ]}>
<Button disabled={isReply} loading={loading} paused={paused} onPress={togglePlayPause} theme={theme} /> <Button disabled={isReply} loading={loading} paused={paused} onPress={togglePlayPause} theme={theme} />
<Slider <Slider
value={currentTime} value={currentPosition}
maximumValue={duration} maximumValue={duration}
onValueChange={onValueChange} onValueChange={onValueChange}
thumbTintColor={thumbColor} thumbTintColor={thumbColor}
@ -260,20 +295,10 @@ IMessageAudioProps) => {
disabled={isReply} disabled={isReply}
maximumTrackTintColor={themes[theme].auxiliaryText} maximumTrackTintColor={themes[theme].auxiliaryText}
animationConfig={sliderAnimatedConfig} animationConfig={sliderAnimatedConfig}
onSlidingStart={() => setIsSliding(true)}
onSlidingEnd={onSlidingEnd}
// thumbImage={isIOS ? { uri: 'audio_thumb', scale } : undefined} // thumbImage={isIOS ? { uri: 'audio_thumb', scale } : undefined}
/> />
{/* <Slider
disabled={isReply}
style={styles.slider}
value={position}
maximumValue={duration}
minimumValue={0}
thumbTintColor={thumbColor}
minimumTrackTintColor={themes[theme].tintColor}
maximumTrackTintColor={themes[theme].auxiliaryText}
onValueChange={onValueChange}
thumbImage={isIOS ? { uri: 'audio_thumb', scale } : undefined}
/> */}
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{getDuration()}</Text> <Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{getDuration()}</Text>
</View> </View>
</> </>

View File

@ -1,5 +1,6 @@
import TrackPlayer, { Event, State, Capability } from 'react-native-track-player'; import TrackPlayer, { Event, State, Capability } from 'react-native-track-player';
// import type { ProgressUpdateEvent } from 'react-native-track-player';
import { clearCurrentTrack } from './tracksStorage';
let wasPausedByDuck = false; let wasPausedByDuck = false;
@ -14,6 +15,11 @@ export const playbackService = async () => {
TrackPlayer.play(); TrackPlayer.play();
}); });
TrackPlayer.addEventListener(Event.RemoteStop, () => {
clearCurrentTrack();
TrackPlayer.destroy();
});
TrackPlayer.addEventListener(Event.RemoteDuck, async e => { TrackPlayer.addEventListener(Event.RemoteDuck, async e => {
if (e.permanent === true) { if (e.permanent === true) {
TrackPlayer.stop(); TrackPlayer.stop();
@ -26,47 +32,13 @@ export const playbackService = async () => {
wasPausedByDuck = false; wasPausedByDuck = false;
} }
}); });
// TrackPlayer.addEventListener(Event.PlaybackQueueEnded, data => {
// console.log('Event.PlaybackQueueEnded', data);
// });
// TrackPlayer.addEventListener(Event.PlaybackTrackChanged, data => {
// console.log('Event.PlaybackTrackChanged', data);
// });
// TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, (data: ProgressUpdateEvent) => {
// console.log('Event.PlaybackProgressUpdated', data);
// });
// TrackPlayer.addEventListener(Event.RemoteNext, () => {
// TrackPlayer.skipToNext();
// });
// TrackPlayer.addEventListener(Event.RemotePrevious, () => {
// TrackPlayer.skipToPrevious();
// });
}; };
export const setupService = async () => { export const setupService = async () => {
try {
await TrackPlayer.getCurrentTrack();
} catch {
await TrackPlayer.setupPlayer(); await TrackPlayer.setupPlayer();
await TrackPlayer.updateOptions({ await TrackPlayer.updateOptions({
stopWithApp: false, stopWithApp: false,
capabilities: [ capabilities: [Capability.Play, Capability.Pause, Capability.Stop],
Capability.Play, compactCapabilities: [Capability.Play, Capability.Pause]
Capability.Pause,
Capability.Stop
// Capability.SkipToNext,
// Capability.SkipToPrevious
],
compactCapabilities: [
Capability.Play,
Capability.Pause
// Capability.SkipToNext
]
}); });
}
}; };

View File

@ -1,58 +0,0 @@
import MMKVStorage from 'react-native-mmkv-storage';
export const TracksStorage = new MMKVStorage.Loader().withInstanceID('tracks').initialize();
interface Track {
trackId: string;
isPlaying: boolean;
title?: string;
artist?: string;
}
export const initializeTracks = () => {
const tracks = TracksStorage.getArray('tracks');
if (!tracks) TracksStorage.setArray('tracks', []);
};
export const addTrack = (track: Track) => {
initializeTracks();
const tracks: Track[] = TracksStorage.getArray('tracks') || [];
if (tracks.find((t: Track) => t.trackId === track.trackId)) {
return;
}
TracksStorage.setArray('tracks', [...tracks, track]);
};
export const getTrack = (track: Track) => {
const tracks: Track[] = TracksStorage.getArray('tracks') || [];
return tracks.find((t: Track) => t.trackId === track.trackId);
};
export const updateTrack = (track: Track) => {
const tracks: Track[] = TracksStorage.getArray('tracks') || [];
const index = tracks.findIndex((t: Track) => t.trackId === track.trackId);
if (index !== -1) {
tracks[index] = track;
}
TracksStorage.setArray('tracks', tracks);
};
export const clearTracks = () => {
TracksStorage.setArray('tracks', []);
};
export const getCurrentTrack: () => Track | undefined = () => {
const tracks: Track[] = TracksStorage.getArray('tracks') || [];
const currentTrack = tracks.find((t: Track) => t.isPlaying);
return currentTrack;
};
export const setCurrentTrack = (trackId: string) => {
const tracks: Track[] = TracksStorage.getArray('tracks') || [];
const currentTrack = tracks.find((t: Track) => t.isPlaying);
const trackToToggle = tracks.find((t: Track) => t.trackId === trackId);
currentTrack && updateTrack({ ...currentTrack, isPlaying: false });
if (trackToToggle) {
updateTrack({ ...trackToToggle, isPlaying: true });
}
};

View File

@ -0,0 +1,9 @@
import MMKVStorage, { create } from 'react-native-mmkv-storage';
const TracksStorage = new MMKVStorage.Loader().withInstanceID('tracks').initialize();
export const useTracks = create(TracksStorage);
export const clearCurrentTrack = () => {
TracksStorage.removeItem('currentTrack');
};

View File

@ -8,6 +8,21 @@ jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock
jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); jest.mock('@react-native-clipboard/clipboard', () => mockClipboard);
jest.mock('react-native-mmkv-storage', () => ({
Loader: jest.fn().mockImplementation(() => ({
setProcessingMode: jest.fn().mockImplementation(() => ({
withEncryption: jest.fn().mockImplementation(() => ({
initialize: jest.fn()
}))
})),
withInstanceID: jest.fn().mockImplementation(() => ({
initialize: jest.fn()
}))
})),
create: jest.fn(),
MODES: { MULTI_PROCESS: '' }
}));
jest.mock('rn-fetch-blob', () => ({ jest.mock('rn-fetch-blob', () => ({
fs: { fs: {
dirs: { dirs: {
@ -67,3 +82,47 @@ jest.mock('react-native-math-view', () => {
MathText: react.View // {...} Named export MathText: react.View // {...} Named export
}; };
}); });
jest.mock('react-native-track-player', () => ({
__esModule: true,
default: {
addEventListener: () => ({
remove: jest.fn()
}),
registerEventHandler: jest.fn(),
registerPlaybackService: jest.fn(),
setupPlayer: jest.fn(),
destroy: jest.fn(),
updateOptions: jest.fn(),
reset: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
skip: jest.fn(),
skipToNext: jest.fn(),
skipToPrevious: jest.fn(),
removeUpcomingTracks: jest.fn(),
play: jest.fn(),
pause: jest.fn(),
stop: jest.fn(),
seekTo: jest.fn(),
setVolume: jest.fn(),
setRate: jest.fn(),
getQueue: jest.fn(),
getTrack: jest.fn(),
getCurrentTrack: jest.fn(),
getVolume: jest.fn(),
getDuration: jest.fn(),
getPosition: jest.fn(),
getBufferedPosition: jest.fn(),
getState: jest.fn(),
getRate: jest.fn()
},
useProgress: () => ({
position: 100
})
}));
jest.mock('./app/containers/message/Components/Audio/tracksStorage.ts', () => ({
useTracks: () => ({
currentTrack: ''
})
}));