diff --git a/package.json b/package.json index b0c0b36827..35636d2e5d 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@fortawesome/react-native-fontawesome": "^0.3.2", "@haileyok/bluesky-video": "0.3.2", "@ipld/dag-cbor": "^9.2.0", + "@legendapp/motion": "^2.4.0", "@lingui/react": "^4.14.1", "@mattermost/react-native-paste-input": "mattermost/react-native-paste-input", "@miblanchard/react-native-slider": "^2.6.0", diff --git a/src/components/Toast/index.web.tsx b/src/components/Toast/index.web.tsx index 51a4b6a1bb..421c648a72 100644 --- a/src/components/Toast/index.web.tsx +++ b/src/components/Toast/index.web.tsx @@ -5,6 +5,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {AccessibilityInfo, Pressable, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {Motion} from '@legendapp/motion' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {nanoid} from 'nanoid/non-secure' @@ -23,6 +24,9 @@ import { export {useToast} from '#/components/Toast/Context' export {Outlet} from '#/components/Toast/Portal' +const ANIMATION_DURATION = 300 +const TOAST_GAP = 8 // Gap between toasts + const TOAST_ANIMATION_STYLES = { entering: { animation: 'toastFadeIn 0.3s ease-out forwards', @@ -48,22 +52,54 @@ export function ToastProvider({children}: {children: React.ReactNode}) { const [toasts, setToasts] = useState< { id: string - toast: ToastProps - timeout: NodeJS.Timeout + props: ToastProps + dimensions: { + width: number | undefined + height: number | undefined + } + setDimensions: (width: number, height: number) => void + exiting: boolean + timers: NodeJS.Timeout[] }[] >([]) const show = useCallback(props => { setToasts(prevToasts => { const id = nanoid() + const duration = props.duration || DEFAULT_TOAST_DURATION return [ ...prevToasts, { id, - toast: props, - timeout: setTimeout(() => { - setToasts(currentToasts => currentToasts.filter(t => t.id !== id)) - }, props.duration || DEFAULT_TOAST_DURATION), + props, + 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)) + }, duration + ANIMATION_DURATION), + ], }, ] }) @@ -72,8 +108,8 @@ export function ToastProvider({children}: {children: React.ReactNode}) { useEffect(() => { return () => { setToasts(toasts => { - toasts.map(({timeout}) => { - clearTimeout(timeout) + toasts.map(({timers}) => { + timers.map(clearTimeout) }) return toasts }) @@ -97,7 +133,6 @@ export function ToastProvider({children}: {children: React.ReactNode}) { - {toasts.map(({id, toast}) => ( - - ))} + {toasts.map(toast => { + const isReady = !!toast.dimensions.width + const toastComponent = ( + + ) + + if (!isReady) { + return ( + + { + if (toast.dimensions.width) return + toast.setDimensions( + e.nativeEvent.layout.width, + e.nativeEvent.layout.height, + ) + }}> + {toastComponent} + + + ) + } + + return ( + + + + + + ) + })} )} diff --git a/yarn.lock b/yarn.lock index d5c203fa42..477f47e190 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5013,6 +5013,18 @@ resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" 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": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"