/ components / SwipeChapterItem.tsx
SwipeChapterItem.tsx
1 import React, { useRef, useEffect } from 'react'; 2 import { 3 StyleSheet, 4 Text, 5 View, 6 TouchableOpacity, 7 Animated, 8 } from 'react-native'; 9 import { Ionicons } from '@expo/vector-icons'; 10 import Swipeable from 'react-native-gesture-handler/Swipeable'; 11 12 interface SwipeableChapterItemProps { 13 chapter: { 14 number: string; 15 title: string; 16 date: string; 17 }; 18 isRead: boolean; 19 isLastItem: boolean; 20 isCurrentlyLastRead?: boolean; 21 onPress: () => void; 22 onLongPress: () => void; 23 onUnread: () => void; 24 colors: any; 25 styles: any; 26 currentlyOpenSwipeable: Swipeable | null; 27 setCurrentlyOpenSwipeable: (swipeable: Swipeable | null) => void; 28 } 29 30 const BUTTON_WIDTH = 75; 31 32 const SwipeableChapterItem: React.FC<SwipeableChapterItemProps> = ({ 33 chapter, 34 isRead, 35 isLastItem, 36 isCurrentlyLastRead = false, 37 onPress, 38 onLongPress, 39 onUnread, 40 colors, 41 styles: parentStyles, 42 currentlyOpenSwipeable, 43 setCurrentlyOpenSwipeable, 44 }) => { 45 const swipeableRef = useRef<Swipeable>(null); 46 47 useEffect(() => { 48 if ( 49 currentlyOpenSwipeable && 50 currentlyOpenSwipeable !== swipeableRef.current 51 ) { 52 swipeableRef.current?.close(); 53 } 54 }, [currentlyOpenSwipeable]); 55 56 const renderRightActions = ( 57 _progress: Animated.AnimatedInterpolation<number>, 58 dragX: Animated.AnimatedInterpolation<number> 59 ) => { 60 const trans = dragX.interpolate({ 61 inputRange: [-BUTTON_WIDTH, 0], 62 outputRange: [0, BUTTON_WIDTH], 63 extrapolate: 'clamp', 64 }); 65 66 return ( 67 <Animated.View 68 style={[ 69 styles.rightActionContainer, 70 { 71 transform: [{ translateX: trans }], 72 }, 73 ]} 74 > 75 <TouchableOpacity 76 style={[styles.button, { backgroundColor: colors.primary }]} 77 onPress={() => { 78 onUnread(); 79 swipeableRef.current?.close(); 80 }} 81 > 82 <Ionicons name="close-circle-outline" size={24} color="white" /> 83 <Text style={styles.buttonText}>Unread</Text> 84 </TouchableOpacity> 85 </Animated.View> 86 ); 87 }; 88 89 // If the chapter is not read, render a non-swipeable view 90 if (!isRead) { 91 return ( 92 <View 93 style={[styles.container, isLastItem && parentStyles.lastChapterItem]} 94 > 95 <TouchableOpacity 96 testID="chapter-item" 97 onPress={onPress} 98 onLongPress={onLongPress} 99 style={[parentStyles.chapterItem, styles.content]} 100 > 101 <View style={parentStyles.chapterInfo}> 102 <Text style={parentStyles.chapterTitle}>{chapter.title}</Text> 103 <Text style={parentStyles.chapterDate}>{chapter.date}</Text> 104 </View> 105 <View style={parentStyles.chapterStatus}> 106 <Ionicons 107 name="ellipse-outline" 108 size={24} 109 color={colors.tabIconDefault} 110 /> 111 </View> 112 </TouchableOpacity> 113 </View> 114 ); 115 } 116 117 return ( 118 <View 119 style={[styles.container, isLastItem && parentStyles.lastChapterItem]} 120 > 121 <Swipeable 122 testID="chapter-item" 123 ref={swipeableRef} 124 renderRightActions={renderRightActions} 125 friction={2} 126 leftThreshold={30} 127 rightThreshold={40} 128 overshootRight={false} 129 onSwipeableOpen={() => { 130 if ( 131 currentlyOpenSwipeable && 132 currentlyOpenSwipeable !== swipeableRef.current 133 ) { 134 currentlyOpenSwipeable.close(); 135 } 136 setCurrentlyOpenSwipeable(swipeableRef.current); 137 }} 138 onSwipeableClose={() => { 139 if (currentlyOpenSwipeable === swipeableRef.current) { 140 setCurrentlyOpenSwipeable(null); 141 } 142 }} 143 > 144 <TouchableOpacity 145 onPress={onPress} 146 onLongPress={onLongPress} 147 style={[ 148 parentStyles.chapterItem, 149 styles.content, 150 isCurrentlyLastRead && styles.lastReadChapterItem, 151 ]} 152 > 153 <View style={parentStyles.chapterInfo}> 154 <Text 155 style={[ 156 parentStyles.chapterTitle, 157 isRead && parentStyles.readChapterTitle, 158 isCurrentlyLastRead && styles.lastReadChapterText, 159 ]} 160 > 161 {chapter.title} 162 </Text> 163 <Text style={parentStyles.chapterDate}>{chapter.date}</Text> 164 </View> 165 <View style={parentStyles.chapterStatus}> 166 <Ionicons 167 name="checkmark-circle" 168 size={24} 169 color={ 170 isCurrentlyLastRead ? colors.primary : colors.primary + '99' 171 } 172 /> 173 </View> 174 </TouchableOpacity> 175 </Swipeable> 176 </View> 177 ); 178 }; 179 180 const styles = StyleSheet.create({ 181 container: { 182 width: '100%', 183 backgroundColor: 'transparent', 184 }, 185 content: { 186 backgroundColor: 'transparent', 187 }, 188 rightActionContainer: { 189 width: BUTTON_WIDTH, 190 height: '100%', 191 }, 192 button: { 193 width: BUTTON_WIDTH, 194 height: '100%', 195 alignItems: 'center', 196 justifyContent: 'center', 197 }, 198 buttonText: { 199 color: 'white', 200 fontSize: 12, 201 marginTop: 4, 202 }, 203 lastReadChapterItem: { 204 backgroundColor: 'rgba(0, 128, 0, 0.05)', 205 }, 206 lastReadChapterText: { 207 fontWeight: '700', 208 }, 209 }); 210 211 export default SwipeableChapterItem;