Native toast rework (#4808)
* rework toast to use reanimated * fix animation on iOS --------- Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>zio/stable
parent
27d712290a
commit
abb709d209
|
@ -145,6 +145,9 @@ export const atoms = {
|
||||||
flex_shrink: {
|
flex_shrink: {
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
|
flex_shrink_0: {
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
justify_start: {
|
justify_start: {
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,87 +1,93 @@
|
||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import Animated, {FadeInUp, FadeOutUp} from 'react-native-reanimated'
|
||||||
import RootSiblings from 'react-native-root-siblings'
|
import RootSiblings from 'react-native-root-siblings'
|
||||||
import React from 'react'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {Animated, StyleSheet, View} from 'react-native'
|
import {
|
||||||
import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome'
|
FontAwesomeIcon,
|
||||||
import {Text} from './text/Text'
|
Props as FontAwesomeProps,
|
||||||
import {colors} from 'lib/styles'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_TEST} from '#/env'
|
import {IS_TEST} from '#/env'
|
||||||
|
|
||||||
const TIMEOUT = 4e3
|
const TIMEOUT = 3.7e3
|
||||||
|
|
||||||
export function show(
|
export function show(
|
||||||
message: string,
|
message: string,
|
||||||
_icon: FontAwesomeProps['icon'] = 'check',
|
icon: FontAwesomeProps['icon'] = 'check',
|
||||||
) {
|
) {
|
||||||
if (IS_TEST) return
|
if (IS_TEST) return
|
||||||
const item = new RootSiblings(<Toast message={message} />)
|
const item = new RootSiblings(<Toast message={message} icon={icon} />)
|
||||||
|
// timeout has some leeway to account for the animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
item.destroy()
|
item.destroy()
|
||||||
}, TIMEOUT)
|
}, TIMEOUT + 1e3)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Toast({message}: {message: string}) {
|
function Toast({
|
||||||
const theme = useTheme()
|
message,
|
||||||
const pal = usePalette('default')
|
icon,
|
||||||
const interp = useAnimatedValue(0)
|
}: {
|
||||||
|
message: string
|
||||||
|
icon: FontAwesomeProps['icon']
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {top} = useSafeAreaInsets()
|
||||||
|
|
||||||
React.useEffect(() => {
|
// for the exit animation to work on iOS the animated component
|
||||||
Animated.sequence([
|
// must not be the root component
|
||||||
Animated.timing(interp, {
|
// so we need to wrap it in a view and unmount the toast ahead of time
|
||||||
toValue: 1,
|
const [alive, setAlive] = useState(true)
|
||||||
duration: 150,
|
|
||||||
useNativeDriver: true,
|
useEffect(() => {
|
||||||
}),
|
setTimeout(() => {
|
||||||
Animated.delay(3700),
|
setAlive(false)
|
||||||
Animated.timing(interp, {
|
}, TIMEOUT)
|
||||||
toValue: 0,
|
}, [])
|
||||||
duration: 150,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start()
|
|
||||||
})
|
|
||||||
|
|
||||||
const opacityStyle = {opacity: interp}
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} pointerEvents="none">
|
<View
|
||||||
<Animated.View
|
style={[a.absolute, {top: top + 15, left: 16, right: 16}]}
|
||||||
style={[
|
pointerEvents="none">
|
||||||
pal.view,
|
{alive && (
|
||||||
pal.border,
|
<Animated.View
|
||||||
styles.toast,
|
entering={FadeInUp}
|
||||||
theme.colorScheme === 'dark' && styles.toastDark,
|
exiting={FadeOutUp}
|
||||||
opacityStyle,
|
style={[
|
||||||
]}>
|
a.flex_1,
|
||||||
<Text type="lg-medium" style={pal.text}>
|
t.atoms.bg,
|
||||||
{message}
|
a.shadow_lg,
|
||||||
</Text>
|
a.rounded_sm,
|
||||||
</Animated.View>
|
t.atoms.border_contrast_medium,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.py_lg,
|
||||||
|
a.border,
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_md,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_shrink_0,
|
||||||
|
a.rounded_full,
|
||||||
|
{width: 32, height: 32},
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
size={16}
|
||||||
|
style={t.atoms.text_contrast_low}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[a.h_full, a.justify_center, a.flex_1]}>
|
||||||
|
<Text style={a.text_md}>{message}</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 60,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
toast: {
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
paddingVertical: 10,
|
|
||||||
borderRadius: 24,
|
|
||||||
borderWidth: 1,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowOffset: {width: 0, height: 4},
|
|
||||||
marginHorizontal: 6,
|
|
||||||
},
|
|
||||||
toastDark: {
|
|
||||||
backgroundColor: colors.gray6,
|
|
||||||
shadowOpacity: 0.5,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
Loading…
Reference in New Issue