diff options
Diffstat (limited to 'src/view/com/util/Toast.tsx')
-rw-r--r-- | src/view/com/util/Toast.tsx | 268 |
1 files changed, 44 insertions, 224 deletions
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index 54ef7042d..37ec6acb5 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -1,234 +1,54 @@ -import {useEffect, useMemo, useRef, useState} from 'react' -import {AccessibilityInfo, View} from 'react-native' -import { - Gesture, - GestureDetector, - GestureHandlerRootView, -} from 'react-native-gesture-handler' -import Animated, { - FadeIn, - FadeOut, - runOnJS, - useAnimatedReaction, - useAnimatedStyle, - useSharedValue, - withDecay, - withSpring, -} from 'react-native-reanimated' -import RootSiblings from 'react-native-root-siblings' -import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {toast} from '#/components/Toast' +import {type ToastType} from '#/components/Toast/types' -import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import { - convertLegacyToastType, - getToastTypeStyles, - type LegacyToastType, - TOAST_ANIMATION_CONFIG, - TOAST_TYPE_TO_ICON, - type ToastType, -} from '#/view/com/util/Toast.style' -import {atoms as a, useTheme} from '#/alf' -import {Text} from '#/components/Typography' - -const TIMEOUT = 2e3 +/** + * @deprecated use {@link ToastType} and {@link toast} instead + */ +export type LegacyToastType = + | 'xmark' + | 'exclamation-circle' + | 'check' + | 'clipboard-check' + | 'circle-exclamation' + +export const convertLegacyToastType = ( + type: ToastType | LegacyToastType, +): ToastType => { + switch (type) { + // these ones are fine + case 'default': + case 'success': + case 'error': + case 'warning': + case 'info': + return type + // legacy ones need conversion + case 'xmark': + return 'error' + case 'exclamation-circle': + return 'warning' + case 'check': + return 'success' + case 'clipboard-check': + return 'success' + case 'circle-exclamation': + return 'warning' + default: + return 'default' + } +} -// Use type overloading to mark certain types as deprecated -sfn -// https://stackoverflow.com/a/78325851/13325987 -export function show(message: string, type?: ToastType): void /** - * @deprecated type is deprecated - use one of `'default' | 'success' | 'error' | 'warning' | 'info'` + * @deprecated use {@link toast} instead */ -export function show(message: string, type?: LegacyToastType): void export function show( message: string, type: ToastType | LegacyToastType = 'default', ): void { - if (process.env.NODE_ENV === 'test') { - return - } - - AccessibilityInfo.announceForAccessibility(message) - const item = new RootSiblings( - ( - <Toast - message={message} - type={convertLegacyToastType(type)} - destroy={() => item.destroy()} - /> - ), - ) -} - -function Toast({ - message, - type, - destroy, -}: { - message: string - type: ToastType - destroy: () => void -}) { - const t = useTheme() - const {top} = useSafeAreaInsets() - const isPanning = useSharedValue(false) - const dismissSwipeTranslateY = useSharedValue(0) - const [cardHeight, setCardHeight] = useState(0) - - const toastStyles = getToastTypeStyles(t) - const colors = toastStyles[type] - const IconComponent = TOAST_TYPE_TO_ICON[type] - - // for the exit animation to work on iOS the animated component - // must not be the root component - // so we need to wrap it in a view and unmount the toast ahead of time - const [alive, setAlive] = useState(true) - - const hideAndDestroyImmediately = () => { - setAlive(false) - setTimeout(() => { - destroy() - }, 1e3) - } - - const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>() - const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => { - clearTimeout(destroyTimeoutRef.current) - destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, TIMEOUT) + const convertedType = convertLegacyToastType(type) + toast.show({ + type: convertedType, + content: message, + a11yLabel: message, }) - const pauseDestroy = useNonReactiveCallback(() => { - clearTimeout(destroyTimeoutRef.current) - }) - - useEffect(() => { - hideAndDestroyAfterTimeout() - }, [hideAndDestroyAfterTimeout]) - - const panGesture = useMemo(() => { - return Gesture.Pan() - .activeOffsetY([-10, 10]) - .failOffsetX([-10, 10]) - .maxPointers(1) - .onStart(() => { - 'worklet' - if (!alive) return - isPanning.set(true) - runOnJS(pauseDestroy)() - }) - .onUpdate(e => { - 'worklet' - if (!alive) return - dismissSwipeTranslateY.value = e.translationY - }) - .onEnd(e => { - 'worklet' - if (!alive) return - runOnJS(hideAndDestroyAfterTimeout)() - isPanning.set(false) - if (e.velocityY < -100) { - if (dismissSwipeTranslateY.value === 0) { - // HACK: If the initial value is 0, withDecay() animation doesn't start. - // This is a bug in Reanimated, but for now we'll work around it like this. - dismissSwipeTranslateY.value = 1 - } - dismissSwipeTranslateY.value = withDecay({ - velocity: e.velocityY, - velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), - deceleration: 1, - }) - } else { - dismissSwipeTranslateY.value = withSpring(0, { - stiffness: 500, - damping: 50, - }) - } - }) - }, [ - dismissSwipeTranslateY, - isPanning, - alive, - hideAndDestroyAfterTimeout, - pauseDestroy, - ]) - - const topOffset = top + 10 - - useAnimatedReaction( - () => - !isPanning.get() && - dismissSwipeTranslateY.get() < -topOffset - cardHeight, - (isSwipedAway, prevIsSwipedAway) => { - 'worklet' - if (isSwipedAway && !prevIsSwipedAway) { - runOnJS(destroy)() - } - }, - ) - - const animatedStyle = useAnimatedStyle(() => { - const translation = dismissSwipeTranslateY.get() - return { - transform: [ - { - translateY: translation > 0 ? translation ** 0.7 : translation, - }, - ], - } - }) - - return ( - <GestureHandlerRootView - style={[a.absolute, {top: topOffset, left: 16, right: 16}]} - pointerEvents="box-none"> - {alive && ( - <Animated.View - entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)} - exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)} - onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)} - accessibilityRole="alert" - accessible={true} - accessibilityLabel={message} - accessibilityHint="" - onAccessibilityEscape={hideAndDestroyImmediately} - style={[ - a.flex_1, - {backgroundColor: colors.backgroundColor}, - a.shadow_sm, - {borderColor: colors.borderColor, borderWidth: 1}, - a.rounded_sm, - animatedStyle, - ]}> - <GestureDetector gesture={panGesture}> - <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_shrink_0, - a.rounded_full, - {width: 32, height: 32}, - a.align_center, - a.justify_center, - { - backgroundColor: colors.backgroundColor, - }, - ]}> - <IconComponent fill={colors.iconColor} size="sm" /> - </View> - <View - style={[ - a.h_full, - a.justify_center, - a.flex_1, - a.justify_center, - ]}> - <Text - style={[a.text_md, a.font_bold, {color: colors.textColor}]} - emoji> - {message} - </Text> - </View> - </View> - </GestureDetector> - </Animated.View> - )} - </GestureHandlerRootView> - ) } |