diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift
index 15993ef29e..e6ebef1d33 100644
--- a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift
+++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift
@@ -9,7 +9,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
}
}
- private var rctScrollView: RCTScrollView?
+ private var scrollView: UIScrollView?
private var rctRefreshCtrl: RCTRefreshControl?
private var cancelGestureRecognizers: [UIGestureRecognizer]?
private var animTimer: Timer?
@@ -68,7 +68,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
}
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
- guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
+ guard let sv = self.scrollView else {
return
}
@@ -113,7 +113,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
}
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
- guard let sv = self.rctScrollView?.scrollView else {
+ guard let sv = self.scrollView else {
return
}
@@ -160,32 +160,49 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
return offset
}
+
+ private func findScrollView(in view: UIView, foundCount: Int) -> UIScrollView? {
+ var foundCount = foundCount
+ if let sv = view as? UIScrollView { return sv }
+ for child in view.subviews {
+ if let found = findScrollView(in: child, foundCount: foundCount) {
+ if foundCount == 1 {
+ print("found sv: \(found)")
+// return found
+ } else {
+ print("found sv: \(found)")
+ foundCount += 1
+ }
+ }
+ }
+ return nil
+ }
func tryFindScrollView() {
- guard let scrollViewTag = scrollViewTag else {
- return
- }
-
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
// Otherwise we might end up with duplicates when we switch back to that scrollview.
self.removeCancelGestureRecognizers()
+
+ guard let sv = self.findScrollView(in: self.superview!.superview!.superview!, foundCount: 0) else {
+ print("⚠️ ExpoScrollForwarder: couldn’t find UIScrollView under tag \(tag)")
+ return
+ }
- self.rctScrollView = self.appContext?
- .findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
- self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
+ self.scrollView = sv
+ self.rctRefreshCtrl = sv.refreshControl as? RCTRefreshControl
self.addCancelGestureRecognizers()
}
func addCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in
- self.rctScrollView?.scrollView?.addGestureRecognizer(r)
+ self.scrollView?.addGestureRecognizer(r)
}
}
func removeCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in
- self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
+ self.scrollView?.removeGestureRecognizer(r)
}
}
@@ -202,7 +219,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
}
func scrollToOffset(_ offset: Int, animated: Bool = true) {
- self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
+ self.scrollView?.scrollRectToVisible(CGRect(x: 0, y: offset, width: 0, height: 0), animated: animated)
}
func stopTimer() {
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 8bcd5143d2..ea50fdfb9c 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -60,7 +60,6 @@ import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-repli
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
-import {ToastContainer} from '#/view/com/util/Toast'
import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf'
@@ -157,7 +156,6 @@ function InnerApp() {
-
diff --git a/src/App.web.tsx b/src/App.web.tsx
index ac15c99503..bbe23e5a56 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -51,7 +51,7 @@ import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
-import {ToastContainer} from '#/view/com/util/Toast'
+import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx
index 4a2ccf5ba5..56c6780ad3 100644
--- a/src/view/com/util/Toast.tsx
+++ b/src/view/com/util/Toast.tsx
@@ -1,7 +1,13 @@
import {useEffect, useMemo, useRef, useState} from 'react'
import {AccessibilityInfo, View} from 'react-native'
-import {Gesture, GestureDetector} from 'react-native-gesture-handler'
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler'
import Animated, {
+ FadeInUp,
+ FadeOutUp,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
@@ -9,13 +15,12 @@ import Animated, {
withDecay,
withSpring,
} from 'react-native-reanimated'
+import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
FontAwesomeIcon,
type Props as FontAwesomeProps,
} from '@fortawesome/react-native-fontawesome'
-import EventEmitter from 'eventemitter3'
-import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a, useTheme} from '#/alf'
@@ -23,37 +28,6 @@ import {Text} from '#/components/Typography'
const TIMEOUT = 2e3
-type ToastProps = {
- message: string
- icon: FontAwesomeProps['icon']
-}
-
-const SHOW_TOAST = 'show-toast'
-
-const ToastEventEmitter = new EventEmitter()
-
-export function ToastContainer() {
- const [toasts, setToasts] = useState<(ToastProps & {key: string})[]>([])
-
- useEffect(() => {
- const listener = ({message, icon}: ToastProps) => {
- setToasts(prev => [...prev, {message, icon, key: nanoid()}])
- }
- ToastEventEmitter.on(SHOW_TOAST, listener)
- return () => {
- ToastEventEmitter.off(SHOW_TOAST, listener)
- }
- }, [])
-
- return toasts.map(toast => (
- setToasts(prev => prev.filter(t => t !== toast))}
- />
- ))
-}
-
export function show(
message: string,
icon: FontAwesomeProps['icon'] = 'check',
@@ -62,7 +36,9 @@ export function show(
return
}
AccessibilityInfo.announceForAccessibility(message)
- ToastEventEmitter.emit(SHOW_TOAST, {message, icon})
+ const item = new RootSiblings(
+ item.destroy()} />,
+ )
}
function Toast({
@@ -77,23 +53,19 @@ function Toast({
const t = useTheme()
const {top} = useSafeAreaInsets()
const isPanning = useSharedValue(false)
- const animationTranslateY = useSharedValue(-200)
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 = () => {
- 'worklet'
- animationTranslateY.set(
- withSpring(
- -200,
- {
- damping: 100,
- stiffness: 800,
- restDisplacementThreshold: 0.01,
- },
- () => runOnJS(destroy)(),
- ),
- )
+ setAlive(false)
+ setTimeout(() => {
+ destroy()
+ }, 1e3)
}
const destroyTimeoutRef = useRef>()
@@ -106,15 +78,8 @@ function Toast({
})
useEffect(() => {
- animationTranslateY.set(
- withSpring(0, {
- damping: 100,
- stiffness: 800,
- restDisplacementThreshold: 0.01,
- }),
- )
hideAndDestroyAfterTimeout()
- }, [hideAndDestroyAfterTimeout, animationTranslateY])
+ }, [hideAndDestroyAfterTimeout])
const panGesture = useMemo(() => {
return Gesture.Pan()
@@ -123,15 +88,18 @@ function Toast({
.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) {
@@ -147,15 +115,15 @@ function Toast({
})
} else {
dismissSwipeTranslateY.value = withSpring(0, {
- damping: 75,
- stiffness: 1000,
- restDisplacementThreshold: 0.01,
+ stiffness: 500,
+ damping: 50,
})
}
})
}, [
dismissSwipeTranslateY,
isPanning,
+ alive,
hideAndDestroyAfterTimeout,
pauseDestroy,
])
@@ -175,7 +143,7 @@ function Toast({
)
const animatedStyle = useAnimatedStyle(() => {
- const translation = dismissSwipeTranslateY.get() + animationTranslateY.get()
+ const translation = dismissSwipeTranslateY.get()
return {
transform: [
{
@@ -186,51 +154,62 @@ function Toast({
})
return (
- setCardHeight(evt.nativeEvent.layout.height)}
- accessibilityRole="alert"
- accessible={true}
- accessibilityLabel={message}
- accessibilityHint=""
- onAccessibilityEscape={hideAndDestroyImmediately}
- style={[
- a.absolute,
- {top: topOffset, left: 16, right: 16},
- a.flex_1,
- t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg,
- a.shadow_lg,
- t.atoms.border_contrast_medium,
- a.rounded_sm,
- a.border,
- animatedStyle,
- ]}>
-
-
-
+ {alive && (
+
+ setCardHeight(evt.nativeEvent.layout.height)}
+ accessibilityRole="alert"
+ accessible={true}
+ accessibilityLabel={message}
+ accessibilityHint=""
+ onAccessibilityEscape={hideAndDestroyImmediately}
style={[
- a.flex_shrink_0,
- a.rounded_full,
- {width: 32, height: 32},
- a.align_center,
- a.justify_center,
- {
- backgroundColor:
- t.name === 'dark' ? t.palette.black : t.palette.primary_50,
- },
+ a.flex_1,
+ t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg,
+ a.shadow_lg,
+ t.atoms.border_contrast_medium,
+ a.rounded_sm,
+ a.border,
+ animatedStyle,
]}>
-
-
-
-
- {message}
-
-
-
-
-
+
+
+
+
+
+
+
+ {message}
+
+
+
+
+
+
+ )}
+
)
}
diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx
index 4da1c5d235..7e22fcefc1 100644
--- a/src/view/com/util/Toast.web.tsx
+++ b/src/view/com/util/Toast.web.tsx
@@ -9,6 +9,7 @@ import {
type FontAwesomeIconStyle,
type Props as FontAwesomeProps,
} from '@fortawesome/react-native-fontawesome'
+import type React from 'react'
const DURATION = 3500
@@ -25,7 +26,8 @@ let toastTimeout: NodeJS.Timeout | undefined
// components
// =
-export function ToastContainer() {
+type ToastContainerProps = {}
+export const ToastContainer: React.FC = ({}) => {
const [activeToast, setActiveToast] = useState()
useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => {