diff --git a/src/App.native.tsx b/src/App.native.tsx
index 4037eedd27..3aca0524a0 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -73,6 +73,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 {ToastProvider} from '#/components/Toast'
import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
@@ -133,59 +134,61 @@ function InnerApp() {
-
-
-
-
-
-
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 2897aa3f2f..8edeeb74d1 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -61,7 +61,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 {ToastContainer, ToastProvider} from '#/components/Toast'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
@@ -112,28 +112,28 @@ function InnerApp() {
-
-
-
-
-
-
-
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
@@ -144,26 +144,26 @@ function InnerApp() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -201,7 +201,9 @@ function App() {
-
+
+
+
diff --git a/src/components/Toast/Context.tsx b/src/components/Toast/Context.tsx
new file mode 100644
index 0000000000..1fd593fae2
--- /dev/null
+++ b/src/components/Toast/Context.tsx
@@ -0,0 +1,13 @@
+import {createContext, useContext} from 'react'
+
+import {type ToastApi} from '#/components/Toast/types'
+
+export type ToastContext = ToastApi
+
+export const Context = createContext({
+ show() {},
+})
+
+export function useToast() {
+ return useContext(Context)
+}
diff --git a/src/components/Toast/Portal.tsx b/src/components/Toast/Portal.tsx
new file mode 100644
index 0000000000..31e0452fc4
--- /dev/null
+++ b/src/components/Toast/Portal.tsx
@@ -0,0 +1,7 @@
+import {createPortalGroup} from '#/components/Portal'
+
+const portal = createPortalGroup()
+
+export const Provider = portal.Provider
+export const Outlet = portal.Outlet
+export const Portal = portal.Portal
diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx
index 844d7f900f..57bb56e30c 100644
--- a/src/components/Toast/index.tsx
+++ b/src/components/Toast/index.tsx
@@ -1,5 +1,5 @@
-import {useEffect, useMemo, useRef, useState} from 'react'
-import {AccessibilityInfo} from 'react-native'
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
+import {AccessibilityInfo, View} from 'react-native'
import {
Gesture,
GestureDetector,
@@ -18,15 +18,100 @@ import Animated, {
} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
+import {Context} from '#/components/Toast/Context'
+import * as Portal from '#/components/Toast/Portal'
import {Toast} from '#/components/Toast/Toast'
-import {type ToastApi, type ToastType} from '#/components/Toast/types'
+import {
+ type ToastApi,
+ type ToastProps,
+ type ToastType,
+} from '#/components/Toast/types'
+
+export {useToast} from '#/components/Toast/Context'
+export {Outlet} from '#/components/Toast/Portal'
const TOAST_ANIMATION_DURATION = 300
+export function ToastProvider({children}: {children: React.ReactNode}) {
+ const {top} = useSafeAreaInsets()
+ const [toasts, setToasts] = useState<
+ {
+ id: string
+ toast: ToastProps
+ timeout: NodeJS.Timeout
+ }[]
+ >([])
+
+ const show = useCallback(props => {
+ AccessibilityInfo.announceForAccessibility(props.a11yLabel)
+
+ setToasts(prevToasts => {
+ const id = nanoid()
+ return [
+ ...prevToasts,
+ {
+ id,
+ toast: props,
+ timeout: setTimeout(() => {
+ setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
+ }, props.duration || DEFAULT_TOAST_DURATION),
+ },
+ ]
+ })
+ }, [])
+
+ useEffect(() => {
+ return () => {
+ setToasts(toasts => {
+ toasts.map(({timeout}) => {
+ clearTimeout(timeout)
+ })
+ return toasts
+ })
+ }
+ }, [])
+
+ const ctx = useMemo(
+ () => ({
+ show,
+ }),
+ [show],
+ )
+
+ return (
+
+
+ {children}
+
+ {toasts.length ? (
+
+
+ {toasts.map(({id, toast}) => (
+
+ ))}
+
+
+ ) : null}
+
+
+ )
+}
+
export function ToastContainer() {
return null
}
diff --git a/src/components/Toast/index.web.tsx b/src/components/Toast/index.web.tsx
index f2517e28da..51a4b6a1bb 100644
--- a/src/components/Toast/index.web.tsx
+++ b/src/components/Toast/index.web.tsx
@@ -2,15 +2,26 @@
* Note: relies on styles in #/styles.css
*/
-import {useEffect, useState} from 'react'
+import {useCallback, useEffect, useMemo, useState} from 'react'
import {AccessibilityInfo, Pressable, View} from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {nanoid} from 'nanoid/non-secure'
import {atoms as a, useBreakpoints} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
+import {Context} from '#/components/Toast/Context'
+import * as Portal from '#/components/Toast/Portal'
import {Toast} from '#/components/Toast/Toast'
-import {type ToastApi, type ToastType} from '#/components/Toast/types'
+import {
+ type ToastApi,
+ type ToastProps,
+ type ToastType,
+} from '#/components/Toast/types'
+
+export {useToast} from '#/components/Toast/Context'
+export {Outlet} from '#/components/Toast/Portal'
const TOAST_ANIMATION_STYLES = {
entering: {
@@ -31,6 +42,85 @@ let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
type ToastContainerProps = {}
+export function ToastProvider({children}: {children: React.ReactNode}) {
+ const {bottom} = useSafeAreaInsets()
+ const {gtPhone} = useBreakpoints()
+ const [toasts, setToasts] = useState<
+ {
+ id: string
+ toast: ToastProps
+ timeout: NodeJS.Timeout
+ }[]
+ >([])
+
+ const show = useCallback(props => {
+ setToasts(prevToasts => {
+ const id = nanoid()
+ return [
+ ...prevToasts,
+ {
+ id,
+ toast: props,
+ timeout: setTimeout(() => {
+ setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
+ }, props.duration || DEFAULT_TOAST_DURATION),
+ },
+ ]
+ })
+ }, [])
+
+ useEffect(() => {
+ return () => {
+ setToasts(toasts => {
+ toasts.map(({timeout}) => {
+ clearTimeout(timeout)
+ })
+ return toasts
+ })
+ }
+ }, [])
+
+ const ctx = useMemo(
+ () => ({
+ show,
+ }),
+ [show],
+ )
+
+ return (
+
+
+ {children}
+
+ {toasts.length && (
+
+
+ {toasts.map(({id, toast}) => (
+
+ ))}
+
+
+ )}
+
+
+ )
+}
+
export const ToastContainer: React.FC = ({}) => {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
diff --git a/src/components/Toast/types.ts b/src/components/Toast/types.ts
index 9f1245fa22..062ac7ab2c 100644
--- a/src/components/Toast/types.ts
+++ b/src/components/Toast/types.ts
@@ -1,24 +1,26 @@
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
-export type ToastApi = {
- show: (props: {
- /**
- * The type of toast to show. This determines the styling and icon used.
- */
- type: ToastType
- /**
- * A string, `Text`, or `Span` components to render inside the toast. This
- * allows additional formatting of the content, but should not be used for
- * interactive elements link links or buttons.
- */
- content: React.ReactNode | string
- /**
- * Accessibility label for the toast, used for screen readers.
- */
- a11yLabel: string
- /**
- * Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
- */
- duration?: number
- }) => void
+export type ToastProps = {
+ /**
+ * The type of toast to show. This determines the styling and icon used.
+ */
+ type: ToastType
+ /**
+ * A string, `Text`, or `Span` components to render inside the toast. This
+ * allows additional formatting of the content, but should not be used for
+ * interactive elements link links or buttons.
+ */
+ content: React.ReactNode | string
+ /**
+ * Accessibility label for the toast, used for screen readers.
+ */
+ a11yLabel: string
+ /**
+ * Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
+ */
+ duration?: number
+}
+
+export type ToastApi = {
+ show: (props: ToastProps) => void
}
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index 7a40604f43..896538d397 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -2,6 +2,7 @@ import {View} from 'react-native'
import Animated from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
import {HITSLOP_10} from '#/lib/constants'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
@@ -16,6 +17,7 @@ import {ButtonIcon} from '#/components/Button'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
+import {IS_DEV} from '#/env'
export function HomeHeaderLayoutMobile({
children,
@@ -29,6 +31,7 @@ export function HomeHeaderLayoutMobile({
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
const {hasSession} = useSession()
const playHaptic = useHaptics()
+ const navigate = useNavigation()
return (
{
- playHaptic('Light')
- emitSoftReset()
+ if (IS_DEV) {
+ navigate.navigate('Debug')
+ } else {
+ playHaptic('Light')
+ emitSoftReset()
+ }
}}>
diff --git a/src/view/screens/Storybook/Toasts.tsx b/src/view/screens/Storybook/Toasts.tsx
index 8fc6f095f7..bce8702db7 100644
--- a/src/view/screens/Storybook/Toasts.tsx
+++ b/src/view/screens/Storybook/Toasts.tsx
@@ -4,14 +4,30 @@ import {show as deprecatedShow} from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {toast} from '#/components/Toast'
+import {useToast} from '#/components/Toast/Context'
import {Toast} from '#/components/Toast/Toast'
import {H1} from '#/components/Typography'
export function Toasts() {
+ const {show} = useToast()
return (
Toast Examples
+
+
Dark
+
-
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 543009a55c..325873845a 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -36,6 +36,7 @@ import {
usePolicyUpdateContext,
} from '#/components/PolicyUpdateOverlay'
import {Outlet as PortalOutlet} from '#/components/Portal'
+import {Outlet as ToastPortalOutlet} from '#/components/Toast/Portal'
import {RoutesContainer, TabsNavigator} from '#/Navigation'
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
@@ -172,6 +173,7 @@ function ShellInner() {
<>
+
>
)}
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 3c2bc58abd..8d2533020f 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -27,6 +27,7 @@ import {
usePolicyUpdateContext,
} from '#/components/PolicyUpdateOverlay'
import {Outlet as PortalOutlet} from '#/components/Portal'
+import {Outlet as ToastPortalOutlet} from '#/components/Toast/Portal'
import {FlatNavigator, RoutesContainer} from '#/Navigation'
import {Composer} from './Composer.web'
import {DrawerContent} from './Drawer'
@@ -84,6 +85,7 @@ function ShellInner() {
{policyUpdateState.completed && (
<>
+
>
)}