Bluesky app fork with some witchin' additions 💫

Native toast rework (#4808)

* rework toast to use reanimated

* fix animation on iOS

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>

authored by samuel.fm

Samuel Newman and committed by
GitHub
abb709d2 27d71229

+78 -69
+3
src/alf/atoms.ts
··· 145 145 flex_shrink: { 146 146 flexShrink: 1, 147 147 }, 148 + flex_shrink_0: { 149 + flexShrink: 0, 150 + }, 148 151 justify_start: { 149 152 justifyContent: 'flex-start', 150 153 },
+75 -69
src/view/com/util/Toast.tsx
··· 1 + import React, {useEffect, useState} from 'react' 2 + import {View} from 'react-native' 3 + import Animated, {FadeInUp, FadeOutUp} from 'react-native-reanimated' 1 4 import RootSiblings from 'react-native-root-siblings' 2 - import React from 'react' 3 - import {Animated, StyleSheet, View} from 'react-native' 4 - import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome' 5 - import {Text} from './text/Text' 6 - import {colors} from 'lib/styles' 7 - import {useTheme} from 'lib/ThemeContext' 8 - import {usePalette} from 'lib/hooks/usePalette' 9 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 5 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 + import { 7 + FontAwesomeIcon, 8 + Props as FontAwesomeProps, 9 + } from '@fortawesome/react-native-fontawesome' 10 + 11 + import {atoms as a, useTheme} from '#/alf' 12 + import {Text} from '#/components/Typography' 10 13 import {IS_TEST} from '#/env' 11 14 12 - const TIMEOUT = 4e3 15 + const TIMEOUT = 3.7e3 13 16 14 17 export function show( 15 18 message: string, 16 - _icon: FontAwesomeProps['icon'] = 'check', 19 + icon: FontAwesomeProps['icon'] = 'check', 17 20 ) { 18 21 if (IS_TEST) return 19 - const item = new RootSiblings(<Toast message={message} />) 22 + const item = new RootSiblings(<Toast message={message} icon={icon} />) 23 + // timeout has some leeway to account for the animation 20 24 setTimeout(() => { 21 25 item.destroy() 22 - }, TIMEOUT) 26 + }, TIMEOUT + 1e3) 23 27 } 24 28 25 - function Toast({message}: {message: string}) { 26 - const theme = useTheme() 27 - const pal = usePalette('default') 28 - const interp = useAnimatedValue(0) 29 + function Toast({ 30 + message, 31 + icon, 32 + }: { 33 + message: string 34 + icon: FontAwesomeProps['icon'] 35 + }) { 36 + const t = useTheme() 37 + const {top} = useSafeAreaInsets() 29 38 30 - React.useEffect(() => { 31 - Animated.sequence([ 32 - Animated.timing(interp, { 33 - toValue: 1, 34 - duration: 150, 35 - useNativeDriver: true, 36 - }), 37 - Animated.delay(3700), 38 - Animated.timing(interp, { 39 - toValue: 0, 40 - duration: 150, 41 - useNativeDriver: true, 42 - }), 43 - ]).start() 44 - }) 39 + // for the exit animation to work on iOS the animated component 40 + // must not be the root component 41 + // so we need to wrap it in a view and unmount the toast ahead of time 42 + const [alive, setAlive] = useState(true) 45 43 46 - const opacityStyle = {opacity: interp} 44 + useEffect(() => { 45 + setTimeout(() => { 46 + setAlive(false) 47 + }, TIMEOUT) 48 + }, []) 49 + 47 50 return ( 48 - <View style={styles.container} pointerEvents="none"> 49 - <Animated.View 50 - style={[ 51 - pal.view, 52 - pal.border, 53 - styles.toast, 54 - theme.colorScheme === 'dark' && styles.toastDark, 55 - opacityStyle, 56 - ]}> 57 - <Text type="lg-medium" style={pal.text}> 58 - {message} 59 - </Text> 60 - </Animated.View> 51 + <View 52 + style={[a.absolute, {top: top + 15, left: 16, right: 16}]} 53 + pointerEvents="none"> 54 + {alive && ( 55 + <Animated.View 56 + entering={FadeInUp} 57 + exiting={FadeOutUp} 58 + style={[ 59 + a.flex_1, 60 + t.atoms.bg, 61 + a.shadow_lg, 62 + a.rounded_sm, 63 + t.atoms.border_contrast_medium, 64 + a.rounded_sm, 65 + a.px_md, 66 + a.py_lg, 67 + a.border, 68 + a.flex_row, 69 + a.gap_md, 70 + ]}> 71 + <View 72 + style={[ 73 + a.flex_shrink_0, 74 + a.rounded_full, 75 + {width: 32, height: 32}, 76 + t.atoms.bg_contrast_25, 77 + a.align_center, 78 + a.justify_center, 79 + ]}> 80 + <FontAwesomeIcon 81 + icon={icon} 82 + size={16} 83 + style={t.atoms.text_contrast_low} 84 + /> 85 + </View> 86 + <View style={[a.h_full, a.justify_center, a.flex_1]}> 87 + <Text style={a.text_md}>{message}</Text> 88 + </View> 89 + </Animated.View> 90 + )} 61 91 </View> 62 92 ) 63 93 } 64 - 65 - const styles = StyleSheet.create({ 66 - container: { 67 - position: 'absolute', 68 - top: 60, 69 - left: 0, 70 - right: 0, 71 - alignItems: 'center', 72 - }, 73 - toast: { 74 - paddingHorizontal: 18, 75 - paddingVertical: 10, 76 - borderRadius: 24, 77 - borderWidth: 1, 78 - shadowColor: '#000', 79 - shadowOpacity: 0.1, 80 - shadowOffset: {width: 0, height: 4}, 81 - marginHorizontal: 6, 82 - }, 83 - toastDark: { 84 - backgroundColor: colors.gray6, 85 - shadowOpacity: 0.5, 86 - }, 87 - })