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