Skip to content

Commit 76c8993

Browse files
Update SwipeToConfirm.tsx
This is an AI-generated PR to attempt improving the gesture code. Review carefully.
1 parent c743272 commit 76c8993

File tree

1 file changed

+74
-184
lines changed

1 file changed

+74
-184
lines changed

src/components/SwipeToConfirm.tsx

Lines changed: 74 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@ import React, { ReactElement, memo, useEffect, useState } from 'react';
22
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
33
import { useTranslation } from 'react-i18next';
44
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';
136

147
import { View as ThemedView } from '../styles/components';
158
import { BodySSB } from '../styles/text';
@@ -24,189 +17,86 @@ const INVISIBLE_BORDER = (GRAB_SIZE - CIRCLE_SIZE) / 2;
2417
const PADDING = 8;
2518

2619
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;
3427
}
3528

3629
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,
4437
}: 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+
);
16291
};
16392

16493
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+
},
210100
});
211101

212102
export default memo(SwipeToConfirm);

0 commit comments

Comments
 (0)