1- import React , { useCallback , useMemo , useState } from 'react' ;
1+ import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
22import { StyleSheet , View } from 'react-native' ;
3+ import type { StyleProp , ViewStyle } from 'react-native' ;
34import { Gesture , GestureDetector } from 'react-native-gesture-handler' ;
4- import Animated , {
5- runOnJS ,
6- useAnimatedReaction ,
7- useAnimatedStyle ,
8- useSharedValue ,
9- } from 'react-native-reanimated' ;
5+ import Animated , { runOnJS , useAnimatedStyle , useSharedValue } from 'react-native-reanimated' ;
106
117import { ProgressControlThumb } from './ProgressThumb' ;
128
@@ -50,6 +46,37 @@ const WAVEFORM_GAP = 2;
5046const WAVE_MAX_HEIGHT = 20 ;
5147const WAVE_MIN_HEIGHT = 2 ;
5248
49+ const clampProgress = ( progress : number ) => {
50+ 'worklet' ;
51+ return Math . max ( 0 , Math . min ( progress , 1 ) ) ;
52+ } ;
53+
54+ type WaveformBarsProps = {
55+ color : string ;
56+ heights : number [ ] ;
57+ waveformStyle ?: StyleProp < ViewStyle > ;
58+ } ;
59+
60+ const WaveformBars = React . memo ( ( { color, heights, waveformStyle } : WaveformBarsProps ) => (
61+ < View style = { styles . waveformLayer } >
62+ { heights . map ( ( height , index ) => (
63+ < View
64+ key = { index }
65+ style = { [
66+ styles . waveform ,
67+ {
68+ backgroundColor : color ,
69+ height,
70+ } ,
71+ waveformStyle ,
72+ ] }
73+ />
74+ ) ) }
75+ </ View >
76+ ) ) ;
77+
78+ WaveformBars . displayName = 'WaveformBars' ;
79+
5380export const WaveProgressBar = React . memo (
5481 ( props : WaveProgressBarProps ) => {
5582 const [ width , setWidth ] = useState < number > ( 0 ) ;
@@ -62,30 +89,15 @@ export const WaveProgressBar = React.memo(
6289 progress,
6390 waveformData,
6491 } = props ;
92+ const [ showInteractiveLayer , setShowInteractiveLayer ] = useState (
93+ ( ) => progress > 0 || isPlaying ,
94+ ) ;
6595 const eachWaveformWidth = WAVEFORM_WIDTH + WAVEFORM_GAP ;
6696 const fullWidth = ( amplitudesCount - 1 ) * eachWaveformWidth ;
67- const state = useSharedValue ( progress ) ;
68- const [ currentWaveformProgress , setCurrentWaveformProgress ] = useState < number > ( 0 ) ;
69-
70- const waveFormNumberFromProgress = useCallback (
71- ( progress : number ) => {
72- 'worklet' ;
73- const progressInPrecision = Number ( progress . toFixed ( 2 ) ) ;
74- const progressInWaveformWidth = Number ( ( progressInPrecision * fullWidth ) . toFixed ( 0 ) ) ;
75- const progressInWaveformNumber = Math . floor ( progressInWaveformWidth / 4 ) ;
76- runOnJS ( setCurrentWaveformProgress ) ( progressInWaveformNumber ) ;
77- } ,
78- [ fullWidth ] ,
79- ) ;
80-
81- useAnimatedReaction (
82- ( ) => progress ,
83- ( newProgress ) => {
84- state . value = newProgress ;
85- waveFormNumberFromProgress ( newProgress ) ;
86- } ,
87- [ progress ] ,
88- ) ;
97+ const maxProgressWidth = fullWidth + WAVEFORM_WIDTH ;
98+ const dragStartProgress = useSharedValue ( 0 ) ;
99+ const isDragging = useSharedValue ( false ) ;
100+ const visualProgress = useSharedValue ( progress ) ;
89101
90102 const {
91103 theme : {
@@ -94,42 +106,113 @@ export const WaveProgressBar = React.memo(
94106 } ,
95107 } = useTheme ( ) ;
96108
109+ useEffect ( ( ) => {
110+ if ( ! isDragging . value ) {
111+ visualProgress . value = progress ;
112+ }
113+ } , [ isDragging , progress , visualProgress ] ) ;
114+
115+ useEffect ( ( ) => {
116+ setShowInteractiveLayer ( progress > 0 || isPlaying ) ;
117+ } , [ isPlaying , progress ] ) ;
118+
119+ const handleStartDrag = useCallback (
120+ ( nextProgress : number ) => {
121+ setShowInteractiveLayer ( true ) ;
122+ onStartDrag ?.( nextProgress ) ;
123+ } ,
124+ [ onStartDrag ] ,
125+ ) ;
126+
127+ const handleProgressDrag = useCallback (
128+ ( nextProgress : number ) => {
129+ onProgressDrag ?.( nextProgress ) ;
130+ } ,
131+ [ onProgressDrag ] ,
132+ ) ;
133+
134+ const handleEndDrag = useCallback (
135+ ( nextProgress : number ) => {
136+ onEndDrag ?.( nextProgress ) ;
137+ } ,
138+ [ onEndDrag ] ,
139+ ) ;
140+
97141 const pan = useMemo (
98142 ( ) =>
99143 Gesture . Pan ( )
100144 . maxPointers ( 1 )
101145 . onStart ( ( ) => {
146+ const nextProgress = clampProgress ( visualProgress . value ) ;
147+ dragStartProgress . value = nextProgress ;
148+ isDragging . value = true ;
102149 if ( onStartDrag ) {
103- runOnJS ( onStartDrag ) ( state . value ) ;
150+ runOnJS ( handleStartDrag ) ( nextProgress ) ;
104151 }
105152 } )
106153 . onUpdate ( ( event ) => {
107- const newProgress = Math . max ( 0 , Math . min ( ( state . value + event . x ) / fullWidth , 1 ) ) ;
108- state . value = newProgress ;
109- waveFormNumberFromProgress ( newProgress ) ;
154+ if ( fullWidth <= 0 ) {
155+ return ;
156+ }
157+ const nextProgress = clampProgress (
158+ dragStartProgress . value + event . translationX / fullWidth ,
159+ ) ;
160+ visualProgress . value = nextProgress ;
161+ if ( onProgressDrag ) {
162+ runOnJS ( handleProgressDrag ) ( nextProgress ) ;
163+ }
110164 } )
111165 . onEnd ( ( ) => {
166+ isDragging . value = false ;
112167 if ( onEndDrag ) {
113- runOnJS ( onEndDrag ) ( state . value ) ;
168+ runOnJS ( handleEndDrag ) ( visualProgress . value ) ;
114169 }
115170 } ) ,
116- [ fullWidth , onEndDrag , onStartDrag , state , waveFormNumberFromProgress ] ,
171+ [
172+ dragStartProgress ,
173+ fullWidth ,
174+ handleEndDrag ,
175+ handleProgressDrag ,
176+ handleStartDrag ,
177+ isDragging ,
178+ onEndDrag ,
179+ onProgressDrag ,
180+ onStartDrag ,
181+ visualProgress ,
182+ ] ,
117183 ) ;
118184
119- const stringifiedWaveformData = waveformData . toString ( ) ;
185+ const stringifiedWaveformData = useMemo ( ( ) => waveformData . toString ( ) , [ waveformData ] ) ;
120186
121187 const resampledWaveformData = useMemo (
122188 ( ) => resampleWaveformData ( waveformData , amplitudesCount ) ,
123189 // eslint-disable-next-line react-hooks/exhaustive-deps
124190 [ amplitudesCount , stringifiedWaveformData ] ,
125191 ) ;
126192
193+ const waveformHeights = useMemo (
194+ ( ) =>
195+ resampledWaveformData . map ( ( waveform ) =>
196+ waveform * WAVE_MAX_HEIGHT > WAVE_MIN_HEIGHT
197+ ? waveform * WAVE_MAX_HEIGHT
198+ : WAVE_MIN_HEIGHT ,
199+ ) ,
200+ [ resampledWaveformData ] ,
201+ ) ;
202+
203+ const progressOverlayStyles = useAnimatedStyle (
204+ ( ) => ( {
205+ width : clampProgress ( visualProgress . value ) * maxProgressWidth ,
206+ } ) ,
207+ [ maxProgressWidth ] ,
208+ ) ;
209+
127210 const thumbStyles = useAnimatedStyle (
128211 ( ) => ( {
129212 position : 'absolute' ,
130- transform : [ { translateX : currentWaveformProgress * eachWaveformWidth } ] ,
213+ transform : [ { translateX : clampProgress ( visualProgress . value ) * fullWidth } ] ,
131214 } ) ,
132- [ currentWaveformProgress , fullWidth ] ,
215+ [ fullWidth ] ,
133216 ) ;
134217
135218 return (
@@ -140,30 +223,33 @@ export const WaveProgressBar = React.memo(
140223 } }
141224 style = { [ styles . container , container ] }
142225 >
143- { resampledWaveformData . map ( ( waveform , index ) => (
226+ < WaveformBars
227+ color = { semantics . chatWaveformBar }
228+ heights = { waveformHeights }
229+ waveformStyle = { waveformTheme }
230+ />
231+ { showInteractiveLayer ? (
144232 < Animated . View
145- key = { index }
146- style = { [
147- styles . waveform ,
148- {
149- backgroundColor :
150- index < currentWaveformProgress
151- ? semantics . chatWaveformBarPlaying
152- : semantics . chatWaveformBar ,
153- height :
154- waveform * WAVE_MAX_HEIGHT > WAVE_MIN_HEIGHT
155- ? waveform * WAVE_MAX_HEIGHT
156- : WAVE_MIN_HEIGHT ,
157- } ,
158- waveformTheme ,
159- ] }
160- />
161- ) ) }
162- { ( onEndDrag || onProgressDrag ) && (
163- < Animated . View style = { [ thumbStyles , thumb ] } >
164- < ProgressControlThumb isPlaying = { isPlaying } />
233+ pointerEvents = 'none'
234+ style = { [ styles . progressOverlay , progressOverlayStyles ] }
235+ >
236+ < WaveformBars
237+ color = { semantics . chatWaveformBarPlaying }
238+ heights = { waveformHeights }
239+ waveformStyle = { waveformTheme }
240+ />
165241 </ Animated . View >
166- ) }
242+ ) : null }
243+ { ( onEndDrag || onProgressDrag ) &&
244+ ( showInteractiveLayer ? (
245+ < Animated . View style = { [ thumbStyles , thumb ] } >
246+ < ProgressControlThumb isPlaying = { isPlaying } />
247+ </ Animated . View >
248+ ) : (
249+ < View style = { [ styles . idleThumb , thumb ] } >
250+ < ProgressControlThumb isPlaying = { isPlaying } />
251+ </ View >
252+ ) ) }
167253 </ View >
168254 </ GestureDetector >
169255 ) ;
@@ -172,6 +258,9 @@ export const WaveProgressBar = React.memo(
172258 if ( prevProps . amplitudesCount !== nextProps . amplitudesCount ) {
173259 return false ;
174260 }
261+ if ( prevProps . isPlaying !== nextProps . isPlaying ) {
262+ return false ;
263+ }
175264 if ( prevProps . progress !== nextProps . progress ) {
176265 return false ;
177266 } else {
@@ -182,6 +271,21 @@ export const WaveProgressBar = React.memo(
182271
183272const styles = StyleSheet . create ( {
184273 container : {
274+ alignItems : 'center' ,
275+ flexDirection : 'row' ,
276+ position : 'relative' ,
277+ } ,
278+ idleThumb : {
279+ left : 0 ,
280+ position : 'absolute' ,
281+ } ,
282+ progressOverlay : {
283+ left : 0 ,
284+ overflow : 'hidden' ,
285+ position : 'absolute' ,
286+ top : 0 ,
287+ } ,
288+ waveformLayer : {
185289 alignItems : 'center' ,
186290 flexDirection : 'row' ,
187291 gap : WAVEFORM_GAP ,
0 commit comments