mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at tmp-rm-bitdrift 210 lines 6.0 kB view raw
1import {useEffect, useMemo, useRef, useState} from 'react' 2import {AccessibilityInfo, View} from 'react-native' 3import { 4 Gesture, 5 GestureDetector, 6 GestureHandlerRootView, 7} from 'react-native-gesture-handler' 8import Animated, { 9 FadeInUp, 10 FadeOutUp, 11 runOnJS, 12 useAnimatedReaction, 13 useAnimatedStyle, 14 useSharedValue, 15 withDecay, 16 withSpring, 17} from 'react-native-reanimated' 18import RootSiblings from 'react-native-root-siblings' 19import {useSafeAreaInsets} from 'react-native-safe-area-context' 20import { 21 FontAwesomeIcon, 22 Props as FontAwesomeProps, 23} from '@fortawesome/react-native-fontawesome' 24 25import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 26import {atoms as a, useTheme} from '#/alf' 27import {Text} from '#/components/Typography' 28 29const TIMEOUT = 2e3 30 31export function show( 32 message: string, 33 icon: FontAwesomeProps['icon'] = 'check', 34) { 35 if (process.env.NODE_ENV === 'test') { 36 return 37 } 38 AccessibilityInfo.announceForAccessibility(message) 39 const item = new RootSiblings( 40 <Toast message={message} icon={icon} destroy={() => item.destroy()} />, 41 ) 42} 43 44function Toast({ 45 message, 46 icon, 47 destroy, 48}: { 49 message: string 50 icon: FontAwesomeProps['icon'] 51 destroy: () => void 52}) { 53 const t = useTheme() 54 const {top} = useSafeAreaInsets() 55 const isPanning = useSharedValue(false) 56 const dismissSwipeTranslateY = useSharedValue(0) 57 const [cardHeight, setCardHeight] = useState(0) 58 59 // for the exit animation to work on iOS the animated component 60 // must not be the root component 61 // so we need to wrap it in a view and unmount the toast ahead of time 62 const [alive, setAlive] = useState(true) 63 64 const hideAndDestroyImmediately = () => { 65 setAlive(false) 66 setTimeout(() => { 67 destroy() 68 }, 1e3) 69 } 70 71 const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>() 72 const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => { 73 clearTimeout(destroyTimeoutRef.current) 74 destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, TIMEOUT) 75 }) 76 const pauseDestroy = useNonReactiveCallback(() => { 77 clearTimeout(destroyTimeoutRef.current) 78 }) 79 80 useEffect(() => { 81 hideAndDestroyAfterTimeout() 82 }, [hideAndDestroyAfterTimeout]) 83 84 const panGesture = useMemo(() => { 85 return Gesture.Pan() 86 .activeOffsetY([-10, 10]) 87 .failOffsetX([-10, 10]) 88 .maxPointers(1) 89 .onStart(() => { 90 'worklet' 91 if (!alive) return 92 isPanning.set(true) 93 runOnJS(pauseDestroy)() 94 }) 95 .onUpdate(e => { 96 'worklet' 97 if (!alive) return 98 dismissSwipeTranslateY.value = e.translationY 99 }) 100 .onEnd(e => { 101 'worklet' 102 if (!alive) return 103 runOnJS(hideAndDestroyAfterTimeout)() 104 isPanning.set(false) 105 if (e.velocityY < -100) { 106 if (dismissSwipeTranslateY.value === 0) { 107 // HACK: If the initial value is 0, withDecay() animation doesn't start. 108 // This is a bug in Reanimated, but for now we'll work around it like this. 109 dismissSwipeTranslateY.value = 1 110 } 111 dismissSwipeTranslateY.value = withDecay({ 112 velocity: e.velocityY, 113 velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), 114 deceleration: 1, 115 }) 116 } else { 117 dismissSwipeTranslateY.value = withSpring(0, { 118 stiffness: 500, 119 damping: 50, 120 }) 121 } 122 }) 123 }, [ 124 dismissSwipeTranslateY, 125 isPanning, 126 alive, 127 hideAndDestroyAfterTimeout, 128 pauseDestroy, 129 ]) 130 131 const topOffset = top + 10 132 133 useAnimatedReaction( 134 () => 135 !isPanning.get() && 136 dismissSwipeTranslateY.get() < -topOffset - cardHeight, 137 (isSwipedAway, prevIsSwipedAway) => { 138 'worklet' 139 if (isSwipedAway && !prevIsSwipedAway) { 140 runOnJS(destroy)() 141 } 142 }, 143 ) 144 145 const animatedStyle = useAnimatedStyle(() => { 146 const translation = dismissSwipeTranslateY.get() 147 return { 148 transform: [ 149 { 150 translateY: translation > 0 ? translation ** 0.7 : translation, 151 }, 152 ], 153 } 154 }) 155 156 return ( 157 <GestureHandlerRootView 158 style={[a.absolute, {top: topOffset, left: 16, right: 16}]} 159 pointerEvents="box-none"> 160 {alive && ( 161 <Animated.View 162 entering={FadeInUp} 163 exiting={FadeOutUp} 164 style={[a.flex_1]}> 165 <Animated.View 166 onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)} 167 accessibilityRole="alert" 168 accessible={true} 169 accessibilityLabel={message} 170 accessibilityHint="" 171 onAccessibilityEscape={hideAndDestroyImmediately} 172 style={[ 173 a.flex_1, 174 t.atoms.bg, 175 a.shadow_lg, 176 t.atoms.border_contrast_medium, 177 a.rounded_sm, 178 a.border, 179 animatedStyle, 180 ]}> 181 <GestureDetector gesture={panGesture}> 182 <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> 183 <View 184 style={[ 185 a.flex_shrink_0, 186 a.rounded_full, 187 {width: 32, height: 32}, 188 {backgroundColor: t.palette.primary_50}, 189 a.align_center, 190 a.justify_center, 191 ]}> 192 <FontAwesomeIcon 193 icon={icon} 194 size={16} 195 style={t.atoms.text_contrast_medium} 196 /> 197 </View> 198 <View style={[a.h_full, a.justify_center, a.flex_1]}> 199 <Text style={a.text_md} emoji> 200 {message} 201 </Text> 202 </View> 203 </View> 204 </GestureDetector> 205 </Animated.View> 206 </Animated.View> 207 )} 208 </GestureHandlerRootView> 209 ) 210}