From 0ed99b840d8de13465f010a6434dea50c72b3f62 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 3 Jul 2024 19:05:19 -0700 Subject: New user progress guides (#4716) * Add the animated checkmark svg * Add progress guide list and task components * Add ProgressGuide Toast component * Implement progress-guide controller * Add 7 follows to the progress guide * Wire up action captures * Wire up progress-guide persistence * Trigger progress guide on account creation * Clear the progress guide from storage on complete * Add progress guide interstitial, put behind gate * Fix: read progress guide state from prefs * Some defensive type checks * Create separate toast for completion * List tweaks * Only show on Discover * Spacing and progress tweaks * Completely hide when complete * Capture the progress guide in local state, and only render toasts while guide is active * Fix: ensure persisted hydrates into local state * Gate --------- Co-authored-by: Eric Bailey Co-authored-by: Dan Abramov --- src/components/ProgressGuide/Toast.tsx | 169 +++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/components/ProgressGuide/Toast.tsx (limited to 'src/components/ProgressGuide/Toast.tsx') diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx new file mode 100644 index 000000000..346312af5 --- /dev/null +++ b/src/components/ProgressGuide/Toast.tsx @@ -0,0 +1,169 @@ +import React, {useImperativeHandle} from 'react' +import {Pressable, useWindowDimensions, View} from 'react-native' +import Animated, { + Easing, + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isWeb} from '#/platform/detection' +import {atoms as a, useTheme} from '#/alf' +import {Portal} from '#/components/Portal' +import {AnimatedCheck, AnimatedCheckRef} from '../anim/AnimatedCheck' +import {Text} from '../Typography' + +export interface ProgressGuideToastRef { + open(): void + close(): void +} + +export interface ProgressGuideToastProps { + title: string + subtitle?: string + visibleDuration?: number // default 5s +} + +export const ProgressGuideToast = React.forwardRef< + ProgressGuideToastRef, + ProgressGuideToastProps +>(function ProgressGuideToast({title, subtitle, visibleDuration}, ref) { + const t = useTheme() + const {_} = useLingui() + const insets = useSafeAreaInsets() + const [isOpen, setIsOpen] = React.useState(false) + const translateY = useSharedValue(0) + const opacity = useSharedValue(0) + const animatedCheckRef = React.useRef(null) + const timeoutRef = React.useRef() + const winDim = useWindowDimensions() + + /** + * Methods + */ + + const close = React.useCallback(() => { + // clear the timeout, in case this was called imperatively + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = undefined + } + + // animate the opacity then set isOpen to false when done + const setIsntOpen = () => setIsOpen(false) + opacity.value = withTiming( + 0, + { + duration: 400, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(setIsntOpen)(), + ) + }, [setIsOpen, opacity]) + + const open = React.useCallback(() => { + // set isOpen=true to render + setIsOpen(true) + + // animate the vertical translation, the opacity, and the checkmark + const playCheckmark = () => animatedCheckRef.current?.play() + opacity.value = 0 + opacity.value = withTiming( + 1, + { + duration: 100, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(playCheckmark)(), + ) + translateY.value = 0 + translateY.value = withTiming(insets.top + 10, { + duration: 500, + easing: Easing.out(Easing.cubic), + }) + + // start the countdown timer to autoclose + timeoutRef.current = setTimeout(close, visibleDuration || 5e3) + }, [setIsOpen, translateY, opacity, insets, close, visibleDuration]) + + useImperativeHandle( + ref, + () => ({ + open, + close, + }), + [open, close], + ) + + const containerStyle = React.useMemo(() => { + let left = 10 + let right = 10 + if (isWeb && winDim.width > 400) { + left = right = (winDim.width - 380) / 2 + } + return { + position: isWeb ? 'fixed' : 'absolute', + top: 0, + left, + right, + } + }, [winDim.width]) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{translateY: translateY.value}], + opacity: opacity.value, + })) + + return ( + isOpen && ( + + + + + + {title} + {subtitle && ( + + {subtitle} + + )} + + + + + ) + ) +}) -- cgit 1.4.1