From 267cc37e602ddff911637b7af553498addded9f7 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 2 May 2025 11:41:55 -0700 Subject: [PATCH 1/4] Revert "completely rearchitect toasts" This reverts commit 2e2fcaeeed527580a6c485718544b85e8b4f52b9. --- .../ios/ExpoScrollForwarderView.swift | 43 ++-- src/App.native.tsx | 2 - src/App.web.tsx | 2 +- src/view/com/util/Toast.tsx | 187 ++++++++---------- src/view/com/util/Toast.web.tsx | 4 +- 5 files changed, 117 insertions(+), 121 deletions(-) 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..62b5ff80c8 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) => { From a3deb2876e0bbec35fffa282960c33bea0899ac6 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 2 May 2025 11:48:30 -0700 Subject: [PATCH 2/4] final upgrades --- package.json | 4 ++-- yarn.lock | 31 ++----------------------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index c94d132000..0d96e688cb 100644 --- a/package.json +++ b/package.json @@ -224,7 +224,7 @@ "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", - "@expo/config-plugins": "~10.0.0", + "@expo/config-plugins": "~10.0.2", "@lingui/cli": "^4.14.1", "@lingui/macro": "^4.14.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", @@ -247,7 +247,7 @@ "babel-plugin-macros": "^3.1.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.1.0-rc.1", - "babel-preset-expo": "~13.0.0", + "babel-preset-expo": "~13.1.11", "eslint": "^8.19.0", "eslint-plugin-bsky-internal": "link:./eslint", "eslint-plugin-ft-flow": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 66dceefdf7..c85766c36b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3859,7 +3859,7 @@ xcode "^3.0.1" xml2js "0.6.0" -"@expo/config-plugins@~10.0.0", "@expo/config-plugins@~10.0.1", "@expo/config-plugins@~10.0.2": +"@expo/config-plugins@~10.0.1", "@expo/config-plugins@~10.0.2": version "10.0.2" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-10.0.2.tgz#040867991e9c8c527b4f5c13a47bcf040a7479fe" integrity sha512-TzUn3pPdpwCS0yYaSlZOClgDmCX8N4I2lfgitX5oStqmvpPtB+vqtdyqsVM02fQ2tlJIAqwBW+NHaHqqy8Jv7g== @@ -6142,7 +6142,7 @@ "@babel/traverse" "^7.25.3" "@react-native/codegen" "0.79.2" -"@react-native/babel-preset@0.79.0-rc.4", "@react-native/babel-preset@0.79.2": +"@react-native/babel-preset@0.79.2": version "0.79.2" resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.2.tgz#5a683a6efeea357a326f70c84a881be2bafbeae3" integrity sha512-/HNu869oUq4FUXizpiNWrIhucsYZqu0/0spudJEzk9SEKar0EjVDP7zkg/sKK+KccNypDQGW7nFXT8onzvQ3og== @@ -8688,33 +8688,6 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-expo@~13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-13.0.0.tgz#49662765a175994fd70ea89d1f7f6ea2147e5c98" - integrity sha512-4NfamKh+BKu6v0VUtZ2wFuZ9VdaDnYOC+vsAHhUF3ks1jzFLo9TwBqsrkhD129DIHfdhVJnRJah2KRCXEjcrVQ== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/plugin-proposal-decorators" "^7.12.9" - "@babel/plugin-proposal-export-default-from" "^7.24.7" - "@babel/plugin-syntax-export-default-from" "^7.24.7" - "@babel/plugin-transform-export-namespace-from" "^7.25.9" - "@babel/plugin-transform-flow-strip-types" "^7.25.2" - "@babel/plugin-transform-modules-commonjs" "^7.24.8" - "@babel/plugin-transform-object-rest-spread" "^7.24.7" - "@babel/plugin-transform-parameters" "^7.24.7" - "@babel/plugin-transform-private-methods" "^7.24.7" - "@babel/plugin-transform-private-property-in-object" "^7.24.7" - "@babel/plugin-transform-runtime" "^7.24.7" - "@babel/preset-react" "^7.22.15" - "@babel/preset-typescript" "^7.23.0" - "@react-native/babel-preset" "0.79.0-rc.4" - babel-plugin-react-native-web "~0.19.13" - babel-plugin-syntax-hermes-parser "^0.25.1" - babel-plugin-transform-flow-enums "^0.0.2" - debug "^4.3.4" - react-refresh "^0.14.2" - resolve-from "^5.0.0" - babel-preset-expo@~13.1.11: version "13.1.11" resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-13.1.11.tgz#de81e6a621c9f40dcb1e0bf5f1fe111f82c10496" From c5089557798b3627f0664f9bf71adba1d59ec5d3 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 2 May 2025 11:49:47 -0700 Subject: [PATCH 3/4] chore: cleanup yarn lock --- yarn.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index c85766c36b..747087e83a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14041,9 +14041,9 @@ kysely@^0.23.4: integrity sha512-TH+b56pVXQq0tsyooYLeNfV11j6ih7D50dyN8tkM0e7ndiUH28Nziojiog3qRFlmEj9XePYdZUrNJ2079Qjdow== lan-network@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/lan-network/-/lan-network-0.1.4.tgz#e55742ec279f1c622911b9f61f9ace869344c750" - integrity sha512-9EzcRaFzlj3nSwcn2VOdxm30uHBBCgkX8/xzhSIlZXvQ1N1QV2cSwSrL+4bJC/WI+3k1f9SNJuPt4/F/9Au6hQ== + version "0.1.5" + resolved "https://registry.yarnpkg.com/lan-network/-/lan-network-0.1.5.tgz#e781889b7bd4dbedd9126fff3ceddd809a83c3ff" + integrity sha512-CV3k7l8jW0Z1b+G41tB7JInVyJEKQzh/YPl2v9uXpZMusp0aa+rh3OqG77xWuX7+eVBa8PsdTuMznTAssF4qwg== lande@^1.0.10: version "1.0.10" @@ -16767,11 +16767,11 @@ react-native-dotenv@^3.4.11: dotenv "^16.4.5" react-native-drawer-layout@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.1.6.tgz#4a806a83a812a366d042543fa66a3a345a217843" - integrity sha512-gfEFQ7jxIz/2dbLI/HvEOSXRIM0UAUVakwZ7PmgtV98kSXEl7SOGr1J2E5Gv37OIsvYA1Om3k+7Xr/DzX+zmmg== + version "4.1.7" + resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.1.7.tgz#1c741c9bf9c739d6672201692e4ba4839ca0c8ff" + integrity sha512-KeTGZsNEDbOmgo8ICwr1vBmvWjRrRsvbLc2IAfQnW5h5UtxVZVRxY4QaN84BSBQPXm6tQ6AXfII8TCXCv3c0Ew== dependencies: - use-latest-callback "^0.2.1" + use-latest-callback "^0.2.3" react-native-edge-to-edge@1.6.0, react-native-edge-to-edge@^1.6.0: version "1.6.0" @@ -19408,10 +19408,10 @@ use-latest-callback@^0.1.9: resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== -use-latest-callback@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.1.tgz#4d4e6a9e4817b13142834850dcfa8d24ca4569cf" - integrity sha512-QWlq8Is8BGWBf883QOEQP5HWYX/kMI+JTbJ5rdtvJLmXTIh9XoHIO3PQcmQl8BU44VKxow1kbQUHa6mQSMALDQ== +use-latest-callback@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.3.tgz#2d644d3063040b9bc2d4c55bb525a13ae3de9e16" + integrity sha512-7vI3fBuyRcP91pazVboc4qu+6ZqM8izPWX9k7cRnT8hbD5svslcknsh3S9BUhaK11OmgTV4oWZZVSeQAiV53SQ== use-latest@^1.2.1: version "1.2.1" From 396baa1bb251b386a5473a1e2c898eb25e7f8a2a Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 2 May 2025 13:11:52 -0700 Subject: [PATCH 4/4] prettier --- src/view/com/util/Toast.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx index 62b5ff80c8..7e22fcefc1 100644 --- a/src/view/com/util/Toast.web.tsx +++ b/src/view/com/util/Toast.web.tsx @@ -9,7 +9,7 @@ import { type FontAwesomeIconStyle, type Props as FontAwesomeProps, } from '@fortawesome/react-native-fontawesome' -import type React from 'react'; +import type React from 'react' const DURATION = 3500