Skip to content

Commit a24419d

Browse files
committed
Implement swipe to right
1 parent e1af190 commit a24419d

File tree

8 files changed

+502
-73
lines changed

8 files changed

+502
-73
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import React from 'react';
2+
import { Text, StyleSheet, TouchableOpacity } from 'react-native';
3+
import { PanGestureHandler, State, GestureHandlerRootView } from 'react-native-gesture-handler';
4+
import Animated, {
5+
useAnimatedGestureHandler,
6+
useAnimatedStyle,
7+
useSharedValue,
8+
withSpring,
9+
withTiming,
10+
runOnJS,
11+
interpolateColor
12+
} from 'react-native-reanimated';
13+
14+
import { useTheme } from '../../../theme';
15+
import { type TAnyMessageModel } from '../../../definitions';
16+
import I18n from '../../../i18n';
17+
18+
const SWIPE_THRESHOLD = 80;
19+
const BUTTON_WIDTH = 70;
20+
21+
interface SwipeableMessageProps {
22+
children: React.ReactNode;
23+
message: TAnyMessageModel;
24+
onReply: (message: TAnyMessageModel) => void;
25+
onEdit: (message: TAnyMessageModel) => void;
26+
onQuote: (message: TAnyMessageModel) => void;
27+
onThread: (message: TAnyMessageModel) => void;
28+
canEdit?: boolean;
29+
leftAction?: string;
30+
}
31+
32+
const SwipeableMessage: React.FC<SwipeableMessageProps> = ({
33+
children,
34+
message,
35+
onReply,
36+
onEdit,
37+
onQuote,
38+
onThread,
39+
canEdit = false,
40+
leftAction = 'none'
41+
}) => {
42+
const { colors } = useTheme();
43+
const translateX = useSharedValue(0);
44+
const gestureState = useSharedValue<number>(State.UNDETERMINED);
45+
46+
const canEditMessage = canEdit && message.u?.username && !message.t;
47+
const canPerformSwipeAction = leftAction !== 'edit' || canEditMessage;
48+
49+
const executeAction = (action: string) => {
50+
console.log('SwipeableMessage executeAction:', action, 'canEditMessage:', canEditMessage);
51+
switch (action) {
52+
case 'reply':
53+
onReply(message);
54+
break;
55+
case 'edit':
56+
console.log('Calling onEdit for message:', message.id);
57+
onEdit(message);
58+
break;
59+
case 'quote':
60+
onQuote(message);
61+
break;
62+
case 'thread':
63+
onThread(message);
64+
break;
65+
default:
66+
break;
67+
}
68+
};
69+
70+
const gestureHandler = useAnimatedGestureHandler({
71+
onStart: (_, context: any) => {
72+
context.startX = translateX.value;
73+
},
74+
onActive: (event, context: any) => {
75+
gestureState.value = event.state;
76+
77+
// Only allow left swipe if user can perform the action
78+
if (event.translationX < 0 && canPerformSwipeAction) {
79+
// Left swipe
80+
if (leftAction !== 'none') {
81+
translateX.value = Math.max(event.translationX, -BUTTON_WIDTH);
82+
} else {
83+
translateX.value = 0;
84+
}
85+
} else {
86+
translateX.value = 0;
87+
}
88+
},
89+
onEnd: (event) => {
90+
gestureState.value = State.END;
91+
92+
// Always snap back to original position for safety
93+
translateX.value = withSpring(0, { damping: 20, stiffness: 300 });
94+
95+
// Determine which action to trigger based on swipe direction and distance
96+
const swipeDistance = Math.abs(event.translationX);
97+
98+
if (swipeDistance > SWIPE_THRESHOLD && event.translationX < 0 && canPerformSwipeAction && leftAction !== 'none') {
99+
// Left swipe action
100+
runOnJS(executeAction)(leftAction);
101+
}
102+
}
103+
});
104+
105+
const resetSwipe = () => {
106+
try {
107+
translateX.value = withSpring(0, { damping: 20, stiffness: 300 });
108+
} catch (error) {
109+
console.warn('Error resetting swipe:', error);
110+
translateX.value = 0;
111+
}
112+
};
113+
114+
const animatedStyle = useAnimatedStyle(() => {
115+
return {
116+
transform: [{
117+
translateX: Math.max(translateX.value, -BUTTON_WIDTH)
118+
}]
119+
};
120+
});
121+
122+
const buttonContainerStyle = useAnimatedStyle(() => {
123+
const opacity = translateX.value < 0 ? 1 : 0;
124+
return {
125+
opacity: withTiming(opacity),
126+
transform: [{
127+
translateX: translateX.value + BUTTON_WIDTH
128+
}]
129+
};
130+
});
131+
132+
const replyButtonStyle = useAnimatedStyle(() => {
133+
const backgroundColor = interpolateColor(
134+
translateX.value,
135+
[-BUTTON_WIDTH, 0],
136+
[colors.buttonBackgroundPrimaryDefault || '#007AFF', colors.buttonBackgroundSecondaryDefault || '#F0F0F0']
137+
);
138+
return {
139+
backgroundColor: withTiming(backgroundColor)
140+
};
141+
});
142+
143+
return (
144+
<GestureHandlerRootView style={styles.container}>
145+
{/* Action buttons */}
146+
<Animated.View style={[styles.buttonContainer, buttonContainerStyle]}>
147+
{/* Show action based on left swipe */}
148+
{leftAction !== 'none' && canPerformSwipeAction && (
149+
<TouchableOpacity
150+
style={[styles.actionButton, replyButtonStyle]}
151+
onPress={() => {
152+
try {
153+
executeAction(leftAction);
154+
resetSwipe();
155+
} catch (error) {
156+
console.warn('Error in left action:', error);
157+
resetSwipe();
158+
}
159+
}}
160+
activeOpacity={0.7}
161+
>
162+
<Text style={[styles.buttonText, { color: colors.buttonFontPrimary }]}>
163+
{leftAction === 'thread' ? I18n.t('Create_Thread') : leftAction.charAt(0).toUpperCase() + leftAction.slice(1)}
164+
</Text>
165+
</TouchableOpacity>
166+
)}
167+
</Animated.View>
168+
169+
{/* Message content */}
170+
<PanGestureHandler onGestureEvent={gestureHandler}>
171+
<Animated.View style={[styles.content, animatedStyle]}>
172+
{children}
173+
</Animated.View>
174+
</PanGestureHandler>
175+
</GestureHandlerRootView>
176+
);
177+
};
178+
179+
const styles = StyleSheet.create({
180+
container: {
181+
position: 'relative',
182+
overflow: 'hidden'
183+
},
184+
buttonContainer: {
185+
position: 'absolute',
186+
right: 0,
187+
top: 0,
188+
bottom: 0,
189+
flexDirection: 'row',
190+
alignItems: 'center',
191+
zIndex: 1
192+
},
193+
actionButton: {
194+
width: BUTTON_WIDTH,
195+
height: '100%',
196+
justifyContent: 'center',
197+
alignItems: 'center',
198+
borderRadius: 8
199+
},
200+
buttonText: {
201+
fontSize: 12,
202+
fontWeight: '600'
203+
},
204+
content: {
205+
backgroundColor: 'transparent',
206+
zIndex: 2
207+
}
208+
});
209+
210+
export default SwipeableMessage;

0 commit comments

Comments
 (0)