Bluesky app fork with some witchin' additions 馃挮
at main 4.6 kB view raw
1import React, {useImperativeHandle} from 'react' 2import {Pressable, useWindowDimensions, View} from 'react-native' 3import Animated, { 4 Easing, 5 runOnJS, 6 useAnimatedStyle, 7 useSharedValue, 8 withTiming, 9} from 'react-native-reanimated' 10import {useSafeAreaInsets} from 'react-native-safe-area-context' 11import {msg} from '@lingui/macro' 12import {useLingui} from '@lingui/react' 13 14import {isWeb} from '#/platform/detection' 15import {atoms as a, useTheme} from '#/alf' 16import {Portal} from '#/components/Portal' 17import {AnimatedCheck, type AnimatedCheckRef} from '../anim/AnimatedCheck' 18import {Text} from '../Typography' 19 20export interface ProgressGuideToastRef { 21 open(): void 22 close(): void 23} 24 25export interface ProgressGuideToastProps { 26 title: string 27 subtitle?: string 28 visibleDuration?: number // default 5s 29} 30 31export const ProgressGuideToast = React.forwardRef< 32 ProgressGuideToastRef, 33 ProgressGuideToastProps 34>(function ProgressGuideToast({title, subtitle, visibleDuration}, ref) { 35 const t = useTheme() 36 const {_} = useLingui() 37 const insets = useSafeAreaInsets() 38 const [isOpen, setIsOpen] = React.useState(false) 39 const translateY = useSharedValue(0) 40 const opacity = useSharedValue(0) 41 const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null) 42 const timeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined) 43 const winDim = useWindowDimensions() 44 45 /** 46 * Methods 47 */ 48 49 const close = React.useCallback(() => { 50 // clear the timeout, in case this was called imperatively 51 if (timeoutRef.current) { 52 clearTimeout(timeoutRef.current) 53 timeoutRef.current = undefined 54 } 55 56 // animate the opacity then set isOpen to false when done 57 const setIsntOpen = () => setIsOpen(false) 58 opacity.set(() => 59 withTiming( 60 0, 61 { 62 duration: 400, 63 easing: Easing.out(Easing.cubic), 64 }, 65 () => runOnJS(setIsntOpen)(), 66 ), 67 ) 68 }, [setIsOpen, opacity]) 69 70 const open = React.useCallback(() => { 71 // set isOpen=true to render 72 setIsOpen(true) 73 74 // animate the vertical translation, the opacity, and the checkmark 75 const playCheckmark = () => animatedCheckRef.current?.play() 76 opacity.set(0) 77 opacity.set(() => 78 withTiming( 79 1, 80 { 81 duration: 100, 82 easing: Easing.out(Easing.cubic), 83 }, 84 () => runOnJS(playCheckmark)(), 85 ), 86 ) 87 translateY.set(0) 88 translateY.set(() => 89 withTiming(insets.top + 10, { 90 duration: 500, 91 easing: Easing.out(Easing.cubic), 92 }), 93 ) 94 95 // start the countdown timer to autoclose 96 timeoutRef.current = setTimeout(close, visibleDuration || 5e3) 97 }, [setIsOpen, translateY, opacity, insets, close, visibleDuration]) 98 99 useImperativeHandle( 100 ref, 101 () => ({ 102 open, 103 close, 104 }), 105 [open, close], 106 ) 107 108 const containerStyle = React.useMemo(() => { 109 let left = 10 110 let right = 10 111 if (isWeb && winDim.width > 400) { 112 left = right = (winDim.width - 380) / 2 113 } 114 return { 115 position: isWeb ? 'fixed' : 'absolute', 116 top: 0, 117 left, 118 right, 119 } 120 }, [winDim.width]) 121 122 const animatedStyle = useAnimatedStyle(() => ({ 123 transform: [{translateY: translateY.get()}], 124 opacity: opacity.get(), 125 })) 126 127 return ( 128 isOpen && ( 129 <Portal> 130 <Animated.View 131 style={[ 132 // @ts-ignore position: fixed is web only 133 containerStyle, 134 animatedStyle, 135 ]}> 136 <Pressable 137 style={[ 138 t.atoms.bg, 139 a.flex_row, 140 a.align_center, 141 a.gap_md, 142 a.border, 143 t.atoms.border_contrast_high, 144 a.rounded_md, 145 a.px_lg, 146 a.py_md, 147 a.shadow_sm, 148 { 149 shadowRadius: 8, 150 shadowOpacity: 0.1, 151 shadowOffset: {width: 0, height: 2}, 152 elevation: 8, 153 }, 154 ]} 155 onPress={close} 156 accessibilityLabel={_(msg`Tap to dismiss`)} 157 accessibilityHint=""> 158 <AnimatedCheck 159 fill={t.palette.primary_500} 160 ref={animatedCheckRef} 161 /> 162 <View> 163 <Text style={[a.text_md, a.font_semi_bold]}>{title}</Text> 164 {subtitle && ( 165 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 166 {subtitle} 167 </Text> 168 )} 169 </View> 170 </Pressable> 171 </Animated.View> 172 </Portal> 173 ) 174 ) 175})