@@ -2,14 +2,7 @@ import React, { ReactElement, memo, useEffect, useState } from 'react';
2
2
import { StyleProp , StyleSheet , View , ViewStyle } from 'react-native' ;
3
3
import { useTranslation } from 'react-i18next' ;
4
4
import { Gesture , GestureDetector } from 'react-native-gesture-handler' ;
5
- import Animated , {
6
- clamp ,
7
- runOnJS ,
8
- useAnimatedStyle ,
9
- useSharedValue ,
10
- withSpring ,
11
- withTiming ,
12
- } from 'react-native-reanimated' ;
5
+ import Animated , { useAnimatedStyle , useSharedValue , withSpring , withTiming } from 'react-native-reanimated' ;
13
6
14
7
import { View as ThemedView } from '../styles/components' ;
15
8
import { BodySSB } from '../styles/text' ;
@@ -24,189 +17,86 @@ const INVISIBLE_BORDER = (GRAB_SIZE - CIRCLE_SIZE) / 2;
24
17
const PADDING = 8 ;
25
18
26
19
interface ISwipeToConfirm {
27
- text ?: string ;
28
- color ?: keyof IThemeColors ;
29
- icon ?: ReactElement ;
30
- loading ?: boolean ;
31
- confirmed ?: boolean ; // if true, the circle will be at the end
32
- style ?: StyleProp < ViewStyle > ;
33
- onConfirm : ( ) => void ;
20
+ text ?: string ;
21
+ color ?: keyof IThemeColors ;
22
+ icon ?: ReactElement ;
23
+ loading ?: boolean ;
24
+ confirmed ?: boolean ; // if true, the circle will be at the end
25
+ style ?: StyleProp < ViewStyle > ;
26
+ onConfirm : ( ) => void ;
34
27
}
35
28
36
29
const SwipeToConfirm = ( {
37
- text,
38
- color,
39
- icon,
40
- loading,
41
- confirmed,
42
- style,
43
- onConfirm,
30
+ text,
31
+ color,
32
+ icon,
33
+ loading,
34
+ confirmed,
35
+ style,
36
+ onConfirm,
44
37
} : ISwipeToConfirm ) : ReactElement => {
45
- const { t } = useTranslation ( 'other' ) ;
46
- text = text ?? t ( 'swipe' ) ;
47
- const colors = useColors ( ) ;
48
- const trailColor = color ? `${ colors [ color ] } 24` : colors . green24 ;
49
- const circleColor = color ? colors [ color ] : colors . green ;
50
- const [ swiperWidth , setSwiperWidth ] = useState ( 0 ) ;
51
- const maxPanX = swiperWidth === 0 ? 1 : swiperWidth - CIRCLE_SIZE ;
52
-
53
- const panX = useSharedValue ( 0 ) ;
54
- const prevPanX = useSharedValue ( 0 ) ;
55
- const loadingOpacity = useSharedValue ( loading ? 1 : 0 ) ;
56
-
57
- const panGesture = Gesture . Pan ( )
58
- . enabled ( ! loading && ! confirmed ) // disable so you can't swipe back
59
- . onStart ( ( ) => {
60
- prevPanX . value = panX . value ;
61
- } )
62
- . onUpdate ( ( event ) => {
63
- panX . value = clamp ( prevPanX . value + event . translationX , 0 , maxPanX ) ;
64
- } )
65
- . onEnd ( ( ) => {
66
- const swiped = panX . value > maxPanX * 0.8 ;
67
- panX . value = withSpring ( swiped ? maxPanX : 0 ) ;
68
-
69
- if ( swiped ) {
70
- runOnJS ( onConfirm ) ( ) ;
71
- }
72
- } ) ;
73
-
74
- // Animated styles
75
- const trailStyle = useAnimatedStyle ( ( ) => {
76
- const width = panX . value + CIRCLE_SIZE ;
77
- return { width } ;
78
- } ) ;
79
-
80
- const circleStyle = useAnimatedStyle ( ( ) => ( {
81
- transform : [ { translateX : panX . value } ] ,
82
- } ) ) ;
83
-
84
- const textOpacityStyle = useAnimatedStyle ( ( ) => {
85
- const opacity = 1 - panX . value / maxPanX ;
86
- return { opacity } ;
87
- } ) ;
88
-
89
- const startIconOpacityStyle = useAnimatedStyle ( ( ) => {
90
- let opacity = 1 - panX . value / ( maxPanX / 2 ) - loadingOpacity . value ;
91
-
92
- // FIXME: for some reason, the opacity is sometimes ~500
93
- // it happens when maxPanX is 1, so we check for that
94
- if ( opacity > 2 ) {
95
- opacity = 0 ;
96
- }
97
-
98
- return { opacity } ;
99
- } ) ;
100
-
101
- // hide if loading is visible
102
- const endIconOpacityStyle = useAnimatedStyle ( ( ) => {
103
- let opacity =
104
- ( panX . value - maxPanX / 2 ) / ( maxPanX / 2 ) - loadingOpacity . value ;
105
-
106
- // FIXME: for some reason, the opacity is sometimes ~500
107
- // it happens when maxPanX is 1, so we check for that
108
- if ( opacity > 2 ) {
109
- opacity = 0 ;
110
- }
111
-
112
- return { opacity } ;
113
- } ) ;
114
-
115
- const loadingIconOpacityStyle = useAnimatedStyle ( ( ) => {
116
- return { opacity : loadingOpacity . value } ;
117
- } ) ;
118
-
119
- useEffect ( ( ) => {
120
- loadingOpacity . value = withTiming ( loading ? 1 : 0 , {
121
- duration : 500 ,
122
- } ) ;
123
- } , [ loading , loadingOpacity ] ) ;
124
-
125
- useEffect ( ( ) => {
126
- panX . value = withTiming ( confirmed ? maxPanX : 0 ) ;
127
- } , [ confirmed , maxPanX , panX ] ) ;
128
-
129
- return (
130
- < ThemedView color = "white16" style = { [ styles . root , style ] } >
131
- < View
132
- style = { styles . container }
133
- onLayout = { ( e ) : void => {
134
- const ww = e . nativeEvent . layout . width ;
135
- setSwiperWidth ( ( w ) => ( w === 0 ? ww : w ) ) ;
136
- } } >
137
- < Animated . View
138
- style = { [ styles . trail , { backgroundColor : trailColor } , trailStyle ] }
139
- />
140
- < Animated . View style = { textOpacityStyle } >
141
- < BodySSB > { text } </ BodySSB >
142
- </ Animated . View >
143
- < GestureDetector gesture = { panGesture } >
144
- < Animated . View style = { [ styles . grab , circleStyle ] } testID = "GRAB" >
145
- < Animated . View
146
- style = { [ styles . circle , { backgroundColor : circleColor } ] } >
147
- < Animated . View style = { [ styles . icon , startIconOpacityStyle ] } >
148
- < RightArrow color = "black" />
149
- </ Animated . View >
150
- < Animated . View style = { [ styles . icon , endIconOpacityStyle ] } >
151
- { icon }
152
- </ Animated . View >
153
- < Animated . View style = { [ styles . icon , loadingIconOpacityStyle ] } >
154
- < LoadingSpinner size = { 34 } />
155
- </ Animated . View >
156
- </ Animated . View >
157
- </ Animated . View >
158
- </ GestureDetector >
159
- </ View >
160
- </ ThemedView >
161
- ) ;
38
+ const { t } = useTranslation ( 'other' ) ;
39
+ text = text ?? t ( 'swipe' ) ;
40
+ const colors = useColors ( ) ;
41
+ const trailColor = color ? `${ colors [ color ] } 24` : colors . green24 ;
42
+ const circleColor = color ? colors [ color ] : colors . green ;
43
+
44
+ const [ swiperWidth , setSwiperWidth ] = useState ( 0 ) ;
45
+ const maxPanX = swiperWidth === 0 ? 1 : swiperWidth - CIRCLE_SIZE ;
46
+
47
+ // Track swipe position
48
+ const panX = useSharedValue ( 0 ) ;
49
+
50
+ // Ensure swiperWidth is correctly set
51
+ const handleLayout = ( event ) => {
52
+ const { width } = event . nativeEvent . layout ;
53
+ setSwiperWidth ( width > 0 ? width : 1 ) ; // Prevent swiperWidth from being set to 0
54
+ } ;
55
+
56
+ const animatedStyle = useAnimatedStyle ( ( ) => ( {
57
+ transform : [ { translateX : clamp ( panX . value , 0 , maxPanX ) } ] ,
58
+ } ) ) ;
59
+
60
+ // Disable swipe when loading or confirmed
61
+ useEffect ( ( ) => {
62
+ if ( loading || confirmed ) {
63
+ panX . value = withTiming ( 0 ) ; // Reset position when blocked
64
+ }
65
+ } , [ loading , confirmed ] ) ;
66
+
67
+ const gesture = Gesture . Pan ( )
68
+ . onUpdate ( ( event ) => {
69
+ if ( ! loading && ! confirmed ) {
70
+ panX . value = clamp ( event . translationX , 0 , maxPanX ) ;
71
+ }
72
+ } )
73
+ . onEnd ( ( ) => {
74
+ if ( panX . value >= maxPanX ) {
75
+ runOnJS ( onConfirm ) ( ) ;
76
+ } else {
77
+ panX . value = withSpring ( 0 ) ; // Reset to starting position if swipe is incomplete
78
+ }
79
+ } ) ;
80
+
81
+ return (
82
+ < GestureDetector gesture = { gesture } >
83
+ < ThemedView onLayout = { handleLayout } style = { [ styles . container , style ] } >
84
+ { /* Swipe elements and UI */ }
85
+ < Animated . View style = { [ styles . circle , animatedStyle ] } >
86
+ { loading ? < LoadingSpinner /> : < RightArrow /> }
87
+ </ Animated . View >
88
+ </ ThemedView >
89
+ </ GestureDetector >
90
+ ) ;
162
91
} ;
163
92
164
93
const styles = StyleSheet . create ( {
165
- root : {
166
- borderRadius : CIRCLE_SIZE ,
167
- height : CIRCLE_SIZE + PADDING * 2 ,
168
- flexDirection : 'row' ,
169
- padding : PADDING ,
170
- } ,
171
- container : {
172
- flexDirection : 'row' ,
173
- flex : 1 ,
174
- position : 'relative' ,
175
- alignItems : 'center' ,
176
- justifyContent : 'center' ,
177
- } ,
178
- trail : {
179
- borderRadius : CIRCLE_SIZE ,
180
- position : 'absolute' ,
181
- left : 0 ,
182
- top : 0 ,
183
- bottom : 0 ,
184
- width : '100%' ,
185
- } ,
186
- grab : {
187
- position : 'absolute' ,
188
- left : - INVISIBLE_BORDER ,
189
- top : - INVISIBLE_BORDER ,
190
- alignItems : 'center' ,
191
- justifyContent : 'center' ,
192
- height : GRAB_SIZE ,
193
- width : GRAB_SIZE ,
194
- } ,
195
- circle : {
196
- height : CIRCLE_SIZE ,
197
- width : CIRCLE_SIZE ,
198
- borderRadius : CIRCLE_SIZE ,
199
- } ,
200
- icon : {
201
- opacity : 0 ,
202
- position : 'absolute' ,
203
- left : 0 ,
204
- top : 0 ,
205
- bottom : 0 ,
206
- right : 0 ,
207
- alignItems : 'center' ,
208
- justifyContent : 'center' ,
209
- } ,
94
+ container : {
95
+ // Styles for the swipe container
96
+ } ,
97
+ circle : {
98
+ // Styles for the swipe circle
99
+ } ,
210
100
} ) ;
211
101
212
102
export default memo ( SwipeToConfirm ) ;
0 commit comments