diff --git a/package.json b/package.json
index 6b9d45e317..ee5d276f4a 100644
--- a/package.json
+++ b/package.json
@@ -214,6 +214,8 @@
"react-remove-scroll-bar": "^2.3.8",
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
+ "sonner": "^2.0.7",
+ "sonner-native": "^0.21.0",
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 0b46da9ddf..e22ab3f0e2 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -74,6 +74,7 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
+import {ToastOutlet} from '#/components/Toast'
import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
@@ -165,6 +166,7 @@ function InnerApp() {
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index dfa1e7480f..17434bfcdc 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -62,7 +62,7 @@ import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdate
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
-import {ToastContainer} from '#/components/Toast'
+import {ToastOutlet} from '#/components/Toast'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
@@ -142,6 +142,7 @@ function InnerApp() {
+
@@ -163,7 +164,6 @@ function InnerApp() {
-
diff --git a/src/components/Toast/const.ts b/src/components/Toast/const.ts
index 034d0a2fce..77290a600e 100644
--- a/src/components/Toast/const.ts
+++ b/src/components/Toast/const.ts
@@ -1 +1 @@
-export const DEFAULT_TOAST_DURATION = 3000
+export const DEFAULT_TOAST_DURATION = 3e3
diff --git a/src/components/Toast/index.e2e.tsx b/src/components/Toast/index.e2e.tsx
index 64072d88d0..eff368ac4f 100644
--- a/src/components/Toast/index.e2e.tsx
+++ b/src/components/Toast/index.e2e.tsx
@@ -1,6 +1,6 @@
import {type ToastApi} from '#/components/Toast/types'
-export function ToastContainer() {
+export function ToastOutlet() {
return null
}
diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx
index 131a796b3f..1e73ffe641 100644
--- a/src/components/Toast/index.tsx
+++ b/src/components/Toast/index.tsx
@@ -1,197 +1,29 @@
-import {useEffect, useMemo, useRef, useState} from 'react'
-import {AccessibilityInfo} from 'react-native'
-import {
- Gesture,
- GestureDetector,
- GestureHandlerRootView,
-} from 'react-native-gesture-handler'
-import Animated, {
- Easing,
- runOnJS,
- SlideInUp,
- SlideOutUp,
- 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 {View} from 'react-native'
+import {toast as sonner, Toaster} from 'sonner-native'
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
-import {type ToastApi, type ToastType} from '#/components/Toast/types'
+import {type ToastApi} from '#/components/Toast/types'
-const TOAST_ANIMATION_DURATION = 300
+function ToastOuter({children}: {children: React.ReactNode}) {
+ return {children}
+}
-export function ToastContainer() {
- return null
+export function ToastOutlet() {
+ return (
+
+ )
}
export const toast: ToastApi = {
show(props) {
- if (process.env.NODE_ENV === 'test') {
- return
- }
-
- AccessibilityInfo.announceForAccessibility(props.a11yLabel)
-
- const item = new RootSiblings(
- (
- item.destroy()}
- />
- ),
- )
+ sonner.custom(, {
+ duration: props.duration ?? DEFAULT_TOAST_DURATION,
+ })
},
}
-
-function AnimatedToast({
- type,
- content,
- a11yLabel,
- duration,
- destroy,
-}: {
- type: ToastType
- content: React.ReactNode
- a11yLabel: string
- duration: number
- destroy: () => void
-}) {
- const {top} = useSafeAreaInsets()
- const isPanning = useSharedValue(false)
- const dismissSwipeTranslateY = useSharedValue(0)
- const [cardHeight, setCardHeight] = useState(0)
-
- // 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>()
- const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
- clearTimeout(destroyTimeoutRef.current)
- destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, duration)
- })
- 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 (
-
- {alive && (
- setCardHeight(evt.nativeEvent.layout.height)}
- accessibilityRole="alert"
- accessible={true}
- accessibilityLabel={a11yLabel}
- accessibilityHint=""
- onAccessibilityEscape={hideAndDestroyImmediately}
- style={[a.flex_1, animatedStyle]}>
-
-
-
-
- )}
-
- )
-}
diff --git a/src/components/Toast/index.web.tsx b/src/components/Toast/index.web.tsx
index f2517e28da..b432ca6b0f 100644
--- a/src/components/Toast/index.web.tsx
+++ b/src/components/Toast/index.web.tsx
@@ -1,112 +1,26 @@
-/*
- * Note: relies on styles in #/styles.css
- */
+import {toast as sonner, Toaster} from 'sonner'
-import {useEffect, useState} from 'react'
-import {AccessibilityInfo, Pressable, View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {atoms as a, useBreakpoints} from '#/alf'
+import {atoms as a} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
-import {type ToastApi, type ToastType} from '#/components/Toast/types'
-
-const TOAST_ANIMATION_STYLES = {
- entering: {
- animation: 'toastFadeIn 0.3s ease-out forwards',
- },
- exiting: {
- animation: 'toastFadeOut 0.2s ease-in forwards',
- },
-}
-
-interface ActiveToast {
- type: ToastType
- content: React.ReactNode
- a11yLabel: string
-}
-type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
-let globalSetActiveToast: GlobalSetActiveToast | undefined
-let toastTimeout: NodeJS.Timeout | undefined
-type ToastContainerProps = {}
-
-export const ToastContainer: React.FC = ({}) => {
- const {_} = useLingui()
- const {gtPhone} = useBreakpoints()
- const [activeToast, setActiveToast] = useState()
- const [isExiting, setIsExiting] = useState(false)
-
- useEffect(() => {
- globalSetActiveToast = (t: ActiveToast | undefined) => {
- if (!t && activeToast) {
- setIsExiting(true)
- setTimeout(() => {
- setActiveToast(t)
- setIsExiting(false)
- }, 200)
- } else {
- if (t) {
- AccessibilityInfo.announceForAccessibility(t.a11yLabel)
- }
- setActiveToast(t)
- setIsExiting(false)
- }
- }
- }, [activeToast])
+import {type ToastApi} from '#/components/Toast/types'
+export function ToastOutlet() {
return (
- <>
- {activeToast && (
-
-
- setActiveToast(undefined)}
- />
-
- )}
- >
+
)
}
export const toast: ToastApi = {
show(props) {
- if (toastTimeout) {
- clearTimeout(toastTimeout)
- }
-
- globalSetActiveToast?.({
- type: props.type,
- content: props.content,
- a11yLabel: props.a11yLabel,
+ sonner(, {
+ duration: props.duration ?? DEFAULT_TOAST_DURATION,
+ unstyled: true,
})
-
- toastTimeout = setTimeout(() => {
- globalSetActiveToast?.(undefined)
- }, props.duration || DEFAULT_TOAST_DURATION)
},
}
diff --git a/yarn.lock b/yarn.lock
index d5c203fa42..dc6489bf85 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18178,6 +18178,16 @@ sonic-boom@^3.7.0:
dependencies:
atomic-sleep "^1.0.0"
+sonner-native@^0.21.0:
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/sonner-native/-/sonner-native-0.21.0.tgz#b7968f8a7fdcdbc3b13b478455d63b24a45db829"
+ integrity sha512-pIdyMd722xuDukIvCZCmWh9Jy5FV+m9uKYl3oAzonYE+91eX5556vYrIik5MisDlBniSXaWqC9CnPoS6DKo2Lg==
+
+sonner@^2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6"
+ integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==
+
source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"