Add web animations
This commit is contained in:
@@ -88,6 +88,7 @@
|
|||||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||||
"@haileyok/bluesky-video": "0.3.2",
|
"@haileyok/bluesky-video": "0.3.2",
|
||||||
"@ipld/dag-cbor": "^9.2.0",
|
"@ipld/dag-cbor": "^9.2.0",
|
||||||
|
"@legendapp/motion": "^2.4.0",
|
||||||
"@lingui/react": "^4.14.1",
|
"@lingui/react": "^4.14.1",
|
||||||
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||||
"@miblanchard/react-native-slider": "^2.6.0",
|
"@miblanchard/react-native-slider": "^2.6.0",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {AccessibilityInfo, Pressable, View} from 'react-native'
|
import {AccessibilityInfo, Pressable, View} from 'react-native'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {Motion} from '@legendapp/motion'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
@@ -23,6 +24,9 @@ import {
|
|||||||
export {useToast} from '#/components/Toast/Context'
|
export {useToast} from '#/components/Toast/Context'
|
||||||
export {Outlet} from '#/components/Toast/Portal'
|
export {Outlet} from '#/components/Toast/Portal'
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 300
|
||||||
|
const TOAST_GAP = 8 // Gap between toasts
|
||||||
|
|
||||||
const TOAST_ANIMATION_STYLES = {
|
const TOAST_ANIMATION_STYLES = {
|
||||||
entering: {
|
entering: {
|
||||||
animation: 'toastFadeIn 0.3s ease-out forwards',
|
animation: 'toastFadeIn 0.3s ease-out forwards',
|
||||||
@@ -48,22 +52,54 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
|||||||
const [toasts, setToasts] = useState<
|
const [toasts, setToasts] = useState<
|
||||||
{
|
{
|
||||||
id: string
|
id: string
|
||||||
toast: ToastProps
|
props: ToastProps
|
||||||
timeout: NodeJS.Timeout
|
dimensions: {
|
||||||
|
width: number | undefined
|
||||||
|
height: number | undefined
|
||||||
|
}
|
||||||
|
setDimensions: (width: number, height: number) => void
|
||||||
|
exiting: boolean
|
||||||
|
timers: NodeJS.Timeout[]
|
||||||
}[]
|
}[]
|
||||||
>([])
|
>([])
|
||||||
|
|
||||||
const show = useCallback<ToastApi['show']>(props => {
|
const show = useCallback<ToastApi['show']>(props => {
|
||||||
setToasts(prevToasts => {
|
setToasts(prevToasts => {
|
||||||
const id = nanoid()
|
const id = nanoid()
|
||||||
|
const duration = props.duration || DEFAULT_TOAST_DURATION
|
||||||
return [
|
return [
|
||||||
...prevToasts,
|
...prevToasts,
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
toast: props,
|
props,
|
||||||
timeout: setTimeout(() => {
|
dimensions: {width: undefined, height: undefined},
|
||||||
|
exiting: false,
|
||||||
|
setDimensions: (width: number, height: number) => {
|
||||||
|
setToasts(currentToasts =>
|
||||||
|
currentToasts.map(t => {
|
||||||
|
if (t.id === id) {
|
||||||
|
t.dimensions.width = width
|
||||||
|
t.dimensions.height = height
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
timers: [
|
||||||
|
setTimeout(() => {
|
||||||
|
setToasts(currentToasts =>
|
||||||
|
currentToasts.map(t => {
|
||||||
|
if (t.id === id) {
|
||||||
|
t.exiting = true
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, duration),
|
||||||
|
setTimeout(() => {
|
||||||
setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
|
setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
|
||||||
}, props.duration || DEFAULT_TOAST_DURATION),
|
}, duration + ANIMATION_DURATION),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -72,8 +108,8 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
setToasts(toasts => {
|
setToasts(toasts => {
|
||||||
toasts.map(({timeout}) => {
|
toasts.map(({timers}) => {
|
||||||
clearTimeout(timeout)
|
timers.map(clearTimeout)
|
||||||
})
|
})
|
||||||
return toasts
|
return toasts
|
||||||
})
|
})
|
||||||
@@ -97,7 +133,6 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.fixed,
|
a.fixed,
|
||||||
a.gap_sm,
|
|
||||||
{
|
{
|
||||||
flexDirection: 'column-reverse',
|
flexDirection: 'column-reverse',
|
||||||
left: a.px_xl.paddingLeft,
|
left: a.px_xl.paddingLeft,
|
||||||
@@ -110,9 +145,68 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
]}>
|
]}>
|
||||||
{toasts.map(({id, toast}) => (
|
{toasts.map(toast => {
|
||||||
<Toast key={id} content={toast.content} type={toast.type} />
|
const isReady = !!toast.dimensions.width
|
||||||
))}
|
const toastComponent = (
|
||||||
|
<Toast
|
||||||
|
content={toast.props.content}
|
||||||
|
type={toast.props.type}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={toast.id}
|
||||||
|
style={[a.absolute, a.inset_0, {opacity: 0}]}>
|
||||||
|
<View
|
||||||
|
style={[a.justify_end, {paddingTop: TOAST_GAP}]}
|
||||||
|
onLayout={e => {
|
||||||
|
if (toast.dimensions.width) return
|
||||||
|
toast.setDimensions(
|
||||||
|
e.nativeEvent.layout.width,
|
||||||
|
e.nativeEvent.layout.height,
|
||||||
|
)
|
||||||
|
}}>
|
||||||
|
{toastComponent}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Motion.View
|
||||||
|
key={toast.id}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{
|
||||||
|
y: 0,
|
||||||
|
opacity: toast.exiting ? 0 : 1,
|
||||||
|
height: toast.exiting ? 0 : toast.dimensions.height,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'timing',
|
||||||
|
easing: 'circOut',
|
||||||
|
duration: ANIMATION_DURATION,
|
||||||
|
}}>
|
||||||
|
<View
|
||||||
|
key={toast.id}
|
||||||
|
style={[
|
||||||
|
a.justify_end,
|
||||||
|
{
|
||||||
|
position: toast.exiting ? 'absolute' : 'relative',
|
||||||
|
width: toast.dimensions.width,
|
||||||
|
height: toast.dimensions.height,
|
||||||
|
paddingTop: TOAST_GAP,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Toast
|
||||||
|
content={toast.props.content}
|
||||||
|
type={toast.props.type}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Motion.View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
</Portal.Portal>
|
</Portal.Portal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5013,6 +5013,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c"
|
resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c"
|
||||||
integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==
|
integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==
|
||||||
|
|
||||||
|
"@legendapp/motion@^2.4.0":
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@legendapp/motion/-/motion-2.4.0.tgz#eef6f934b784e07fe94631d5648d412f34fa0c6b"
|
||||||
|
integrity sha512-AAYpRLGvxGD5hIGl9sVHyoUufr66zoH82PuxYcKiPSMdCBI3jwZFWh6CuHjV1leRKVIRk2py1rSvIVabG8eqcw==
|
||||||
|
dependencies:
|
||||||
|
"@legendapp/tools" "2.0.1"
|
||||||
|
|
||||||
|
"@legendapp/tools@2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@legendapp/tools/-/tools-2.0.1.tgz#995fe6cb3e2398b939f645505aa8e1abc84bd07f"
|
||||||
|
integrity sha512-Kxt0HWvWElRK6oybHRzcYxdgaKGwuaiRNreS7usW7QuHXRIHaH4yMcW2YNRG4DHE5fpefv+Bno/BohQcCE4FaA==
|
||||||
|
|
||||||
"@leichtgewicht/ip-codec@^2.0.1":
|
"@leichtgewicht/ip-codec@^2.0.1":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
||||||
|
|||||||
Reference in New Issue
Block a user