Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update SwipeToConfirm.tsx #2221

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 74 additions & 184 deletions src/components/SwipeToConfirm.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import React, { ReactElement, memo, useEffect, useState } from 'react';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';

Check failure on line 2 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

'View' is defined but never used
import { useTranslation } from 'react-i18next';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
clamp,
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated';
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';

Check failure on line 5 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Replace `·useAnimatedStyle,·useSharedValue,·withSpring,·withTiming·` with `⏎↹useAnimatedStyle,⏎↹useSharedValue,⏎↹withSpring,⏎↹withTiming,⏎`

import { View as ThemedView } from '../styles/components';
import { BodySSB } from '../styles/text';

Check failure on line 8 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

'BodySSB' is defined but never used
import { RightArrow } from '../styles/icons';
import { IThemeColors } from '../styles/themes';
import useColors from '../hooks/colors';
Expand All @@ -20,193 +13,90 @@

const CIRCLE_SIZE = 60;
const GRAB_SIZE = 120;
const INVISIBLE_BORDER = (GRAB_SIZE - CIRCLE_SIZE) / 2;

Check failure on line 16 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

'INVISIBLE_BORDER' is assigned a value but never used
const PADDING = 8;

Check failure on line 17 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

'PADDING' is assigned a value but never used

interface ISwipeToConfirm {
text?: string;
color?: keyof IThemeColors;
icon?: ReactElement;
loading?: boolean;
confirmed?: boolean; // if true, the circle will be at the end
style?: StyleProp<ViewStyle>;
onConfirm: () => void;
text?: string;

Check failure on line 20 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Replace `····` with `↹`
color?: keyof IThemeColors;

Check failure on line 21 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Replace `····` with `↹`
icon?: ReactElement;

Check failure on line 22 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Replace `····` with `↹`
loading?: boolean;

Check failure on line 23 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Replace `····` with `↹`
confirmed?: boolean; // if true, the circle will be at the end

Check failure on line 24 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Replace `····` with `↹`
style?: StyleProp<ViewStyle>;
onConfirm: () => void;
}

const SwipeToConfirm = ({
text,
color,
icon,
loading,
confirmed,
style,
onConfirm,
text,
color,
icon,
loading,
confirmed,
style,
onConfirm,
}: ISwipeToConfirm): ReactElement => {
const { t } = useTranslation('other');
text = text ?? t('swipe');
const colors = useColors();
const trailColor = color ? `${colors[color]}24` : colors.green24;
const circleColor = color ? colors[color] : colors.green;
const [swiperWidth, setSwiperWidth] = useState(0);
const maxPanX = swiperWidth === 0 ? 1 : swiperWidth - CIRCLE_SIZE;

const panX = useSharedValue(0);
const prevPanX = useSharedValue(0);
const loadingOpacity = useSharedValue(loading ? 1 : 0);

const panGesture = Gesture.Pan()
.enabled(!loading && !confirmed) // disable so you can't swipe back
.onStart(() => {
prevPanX.value = panX.value;
})
.onUpdate((event) => {
panX.value = clamp(prevPanX.value + event.translationX, 0, maxPanX);
})
.onEnd(() => {
const swiped = panX.value > maxPanX * 0.8;
panX.value = withSpring(swiped ? maxPanX : 0);

if (swiped) {
runOnJS(onConfirm)();
}
});

// Animated styles
const trailStyle = useAnimatedStyle(() => {
const width = panX.value + CIRCLE_SIZE;
return { width };
});

const circleStyle = useAnimatedStyle(() => ({
transform: [{ translateX: panX.value }],
}));

const textOpacityStyle = useAnimatedStyle(() => {
const opacity = 1 - panX.value / maxPanX;
return { opacity };
});

const startIconOpacityStyle = useAnimatedStyle(() => {
let opacity = 1 - panX.value / (maxPanX / 2) - loadingOpacity.value;

// FIXME: for some reason, the opacity is sometimes ~500
// it happens when maxPanX is 1, so we check for that
if (opacity > 2) {
opacity = 0;
}

return { opacity };
});

// hide if loading is visible
const endIconOpacityStyle = useAnimatedStyle(() => {
let opacity =
(panX.value - maxPanX / 2) / (maxPanX / 2) - loadingOpacity.value;

// FIXME: for some reason, the opacity is sometimes ~500
// it happens when maxPanX is 1, so we check for that
if (opacity > 2) {
opacity = 0;
}

return { opacity };
});

const loadingIconOpacityStyle = useAnimatedStyle(() => {
return { opacity: loadingOpacity.value };
});

useEffect(() => {
loadingOpacity.value = withTiming(loading ? 1 : 0, {
duration: 500,
});
}, [loading, loadingOpacity]);

useEffect(() => {
panX.value = withTiming(confirmed ? maxPanX : 0);
}, [confirmed, maxPanX, panX]);

return (
<ThemedView color="white16" style={[styles.root, style]}>
<View
style={styles.container}
onLayout={(e): void => {
const ww = e.nativeEvent.layout.width;
setSwiperWidth((w) => (w === 0 ? ww : w));
}}>
<Animated.View
style={[styles.trail, { backgroundColor: trailColor }, trailStyle]}
/>
<Animated.View style={textOpacityStyle}>
<BodySSB>{text}</BodySSB>
</Animated.View>
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.grab, circleStyle]} testID="GRAB">
<Animated.View
style={[styles.circle, { backgroundColor: circleColor }]}>
<Animated.View style={[styles.icon, startIconOpacityStyle]}>
<RightArrow color="black" />
</Animated.View>
<Animated.View style={[styles.icon, endIconOpacityStyle]}>
{icon}
</Animated.View>
<Animated.View style={[styles.icon, loadingIconOpacityStyle]}>
<LoadingSpinner size={34} />
</Animated.View>
</Animated.View>
</Animated.View>
</GestureDetector>
</View>
</ThemedView>
);
const { t } = useTranslation('other');
text = text ?? t('swipe');
const colors = useColors();
const trailColor = color ? `${colors[color]}24` : colors.green24;
const circleColor = color ? colors[color] : colors.green;

Check warning on line 43 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run lint check

Trailing spaces not allowed
const [swiperWidth, setSwiperWidth] = useState(0);
const maxPanX = swiperWidth === 0 ? 1 : swiperWidth - CIRCLE_SIZE;

// Track swipe position
const panX = useSharedValue(0);

// Ensure swiperWidth is correctly set
const handleLayout = (event) => {
const { width } = event.nativeEvent.layout;
setSwiperWidth(width > 0 ? width : 1); // Prevent swiperWidth from being set to 0
};

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: clamp(panX.value, 0, maxPanX) }],

Check failure on line 57 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run type check

Cannot find name 'clamp'.
}));

// Disable swipe when loading or confirmed
useEffect(() => {
if (loading || confirmed) {
panX.value = withTiming(0); // Reset position when blocked
}
}, [loading, confirmed]);

const gesture = Gesture.Pan()
.onUpdate((event) => {
if (!loading && !confirmed) {
panX.value = clamp(event.translationX, 0, maxPanX);

Check failure on line 70 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run type check

Cannot find name 'clamp'.
}
})
.onEnd(() => {
if (panX.value >= maxPanX) {
runOnJS(onConfirm)();

Check failure on line 75 in src/components/SwipeToConfirm.tsx

View workflow job for this annotation

GitHub Actions / Run type check

Cannot find name 'runOnJS'.
} else {
panX.value = withSpring(0); // Reset to starting position if swipe is incomplete
}
});

return (
<GestureDetector gesture={gesture}>
<ThemedView onLayout={handleLayout} style={[styles.container, style]}>
{/* Swipe elements and UI */}
<Animated.View style={[styles.circle, animatedStyle]}>
{loading ? <LoadingSpinner /> : <RightArrow />}
</Animated.View>
</ThemedView>
</GestureDetector>
);
};

const styles = StyleSheet.create({
root: {
borderRadius: CIRCLE_SIZE,
height: CIRCLE_SIZE + PADDING * 2,
flexDirection: 'row',
padding: PADDING,
},
container: {
flexDirection: 'row',
flex: 1,
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
},
trail: {
borderRadius: CIRCLE_SIZE,
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: '100%',
},
grab: {
position: 'absolute',
left: -INVISIBLE_BORDER,
top: -INVISIBLE_BORDER,
alignItems: 'center',
justifyContent: 'center',
height: GRAB_SIZE,
width: GRAB_SIZE,
},
circle: {
height: CIRCLE_SIZE,
width: CIRCLE_SIZE,
borderRadius: CIRCLE_SIZE,
},
icon: {
opacity: 0,
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
right: 0,
alignItems: 'center',
justifyContent: 'center',
},
container: {
// Styles for the swipe container
},
circle: {
// Styles for the swipe circle
},
});

export default memo(SwipeToConfirm);
Loading