forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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})