diff --git a/.gitignore b/.gitignore index 66658f8e4d..0bd7d137fc 100644 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,7 @@ ios/ .env.* # Firebase (Android) Google services -google-services.json \ No newline at end of file +google-services.json + +# Performance results (Flashlight) +.perf/ \ No newline at end of file diff --git a/__e2e__/maestro/scroll.yaml b/__e2e__/maestro/scroll.yaml new file mode 100644 index 0000000000..2d32793eb0 --- /dev/null +++ b/__e2e__/maestro/scroll.yaml @@ -0,0 +1,77 @@ +# flow.yaml + +appId: xyz.blueskyweb.app +--- +- launchApp +# Login +# - runFlow: +# when: +# - tapOn: "Sign In" +# - tapOn: "Username or email address" +# - inputText: "ansh.bsky.team" +# - tapOn: "Password" +# - inputText: "PASSWORd" +# - tapOn: "Next" +# Allow notifications if popup is visible +# - runFlow: +# when: +# visible: "Notifications" +# commands: +# - tapOn: "Allow" +# Scroll in main feed +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +# Swipe between feeds +- swipe: + direction: "LEFT" +- swipe: + direction: "LEFT" +- swipe: + direction: "LEFT" +- swipe: + direction: "RIGHT" +- swipe: + direction: "RIGHT" +- swipe: + direction: "RIGHT" +# Go to Notifications +- tapOn: + id: "viewHeaderDrawerBtn" +- tapOn: "Notifications" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- swipe: + direction: "DOWN" # Make header visible +# Go to Feeds tab +- tapOn: + id: "viewHeaderDrawerBtn" +- tapOn: "Feeds" +- scrollUntilVisible: + element: "Discover" + direction: UP +- tapOn: "Discover" +- waitForAnimationToEnd +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +# Click on post +- tapOn: + id: "postText" + index: 0 +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" + diff --git a/__mocks__/sentry-expo.js b/__mocks__/sentry-expo.js new file mode 100644 index 0000000000..e735c48c55 --- /dev/null +++ b/__mocks__/sentry-expo.js @@ -0,0 +1,10 @@ +jest.mock('sentry-expo', () => ({ + init: () => jest.fn(), + Native: { + ReactNativeTracing: jest.fn().mockImplementation(() => ({ + start: jest.fn(), + stop: jest.fn(), + })), + ReactNavigationInstrumentation: jest.fn(), + }, +})) diff --git a/babel.config.js b/babel.config.js index 598e2a5671..706fdff5c9 100644 --- a/babel.config.js +++ b/babel.config.js @@ -30,5 +30,10 @@ module.exports = function (api) { ], 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last ], + env: { + production: { + plugins: ['transform-remove-console'], + }, + }, } } diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000000..8af163a8d9 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,14 @@ +# Testing instructions + +### Using Maestro E2E tests +1. Install Maestro by following [these instuctions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests. +2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one. +3. You can also use [Maestro Studio](https://maestro.mobile.dev/getting-started/maestro-studio) which automatically generates commands by recording your actions on the app. Therefore, you can create realistic tests without having to manually write any code. Use the `maestro studio` command to start recording your actions. + + +### Using Flashlight for Performance Testing +1. Make sure Maestro is installed (optional: only for auomated testing) by following the instructions above +2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/) +3. The simplest way to get started is by running `yarn perf:measure` which will run a live preview of the performance test results. You can [see a demo here](https://github.com/bamlab/flashlight/assets/4534323/4038a342-f145-4c3b-8cde-17949bf52612) +4. The `yarn perf:test:measure` will run the `scroll.yaml` test located in `__e2e__/maestro/scroll.yaml` and give the results in `.perf/results.json` which can be viewed by running `yarn:perf:results` +5. You can also run your own tests by running `yarn perf:test ` where `` is the path to your test file. For example, `yarn perf:test __e2e__/maestro/scroll.yaml` will run the `scroll.yaml` test located in `__e2e__/maestro/scroll.yaml`. \ No newline at end of file diff --git a/eas.json b/eas.json index 69e5c94d68..402abeccd2 100644 --- a/eas.json +++ b/eas.json @@ -9,7 +9,7 @@ "distribution": "internal", "ios": { "simulator": true, - "resourceClass": "m-large" + "resourceClass": "large" }, "channel": "development" }, @@ -17,20 +17,20 @@ "developmentClient": true, "distribution": "internal", "ios": { - "resourceClass": "m-large" + "resourceClass": "large" }, "channel": "development" }, "preview": { "distribution": "internal", "ios": { - "resourceClass": "m-large" + "resourceClass": "large" }, "channel": "preview" }, "production": { "ios": { - "resourceClass": "m-large" + "resourceClass": "large" }, "channel": "production" }, diff --git a/jest/jestSetup.js b/jest/jestSetup.js index 2629be2cc5..5d6bd4f1ff 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -74,3 +74,14 @@ jest.mock('lande', () => ({ __esModule: true, // this property makes it work default: jest.fn().mockReturnValue([['eng']]), })) + +jest.mock('sentry-expo', () => ({ + init: () => jest.fn(), + Native: { + ReactNativeTracing: jest.fn().mockImplementation(() => ({ + start: jest.fn(), + stop: jest.fn(), + })), + ReactNavigationInstrumentation: jest.fn(), + }, +})) diff --git a/package.json b/package.json index fdb6480f92..c94c6589f7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "web": "expo start --web", "build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/", "start": "expo start --dev-client", + "start:prod": "expo start --dev-client --no-dev --minify", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", "test": "jest --forceExit --testTimeout=20000 --bail", "test-watch": "jest --watchAll", @@ -22,6 +23,11 @@ "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build": "detox build -c ios.sim.debug", "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", + "perf:test": "maestro test", + "perf:test:run": "maestro test __e2e__/maestro/scroll.yaml", + "perf:test:measure": "flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json", + "perf:test:results": "flashlight report .perf/results.json", + "perf:measure": "flashlight measure", "build:apk": "eas build -p android --profile dev-android-apk" }, "dependencies": { @@ -54,7 +60,7 @@ "@segment/analytics-react": "^1.0.0-rc1", "@segment/analytics-react-native": "^2.10.1", "@segment/sovran-react-native": "^0.4.5", - "@sentry/react-native": "5.5.0", + "@sentry/react-native": "5.10.0", "@tanstack/react-query": "^4.33.0", "@tiptap/core": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220", @@ -72,6 +78,7 @@ "@zxing/text-encoding": "^0.9.0", "array.prototype.findlast": "^1.2.3", "await-lock": "^2.2.2", + "babel-plugin-transform-remove-console": "^6.9.4", "base64-js": "^1.5.1", "bcp-47-match": "^2.0.3", "email-validator": "^2.0.4", @@ -151,7 +158,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-responsive": "^9.0.2", "rn-fetch-blob": "^0.12.0", - "sentry-expo": "~7.0.0", + "sentry-expo": "~7.0.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", "zeego": "^1.6.2", diff --git a/patches/@sentry+react-native+5.5.0.patch b/patches/@sentry+react-native+5.10.0.patch similarity index 92% rename from patches/@sentry+react-native+5.5.0.patch rename to patches/@sentry+react-native+5.10.0.patch index 5ff4ddabad..2962aa44c2 100644 --- a/patches/@sentry+react-native+5.5.0.patch +++ b/patches/@sentry+react-native+5.10.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js -index 7e0b4cd..3fd7406 100644 +index 7e0b4cd..177454c 100644 --- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js +++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js @@ -3,6 +3,8 @@ import { LogBox } from 'react-native'; @@ -12,3 +12,4 @@ index 7e0b4cd..3fd7406 100644 + } catch (e) {} } //# sourceMappingURL=ignorerequirecyclelogs.js.map +\ No newline at end of file diff --git a/patches/react-native-pager-view+6.1.4.patch b/patches/react-native-pager-view+6.1.4.patch index adee2533fa..d6b4178ab3 100644 --- a/patches/react-native-pager-view+6.1.4.patch +++ b/patches/react-native-pager-view+6.1.4.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m -index ab0fc7f..fbbf19f 100644 +index ab0fc7f..1ace752 100644 --- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m +++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m @@ -1,6 +1,6 @@ @@ -19,7 +19,7 @@ index ab0fc7f..fbbf19f 100644 @property(nonatomic, strong) UIPageViewController *reactPageViewController; @property(nonatomic, strong) RCTEventDispatcher *eventDispatcher; -@@ -80,6 +80,10 @@ +@@ -80,6 +80,10 @@ - (void)didMoveToWindow { [self setupInitialController]; } @@ -30,13 +30,13 @@ index ab0fc7f..fbbf19f 100644 if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) { [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer]; } -@@ -463,4 +467,21 @@ +@@ -463,4 +467,21 @@ - (NSString *)determineScrollDirection:(UIScrollView *)scrollView { - (BOOL)isLtrLayout { return [_layoutDirection isEqualToString:@"ltr"]; } + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { -+ if (otherGestureRecognizer == self.scrollView.panGestureRecognizer) { ++ if (!_overdrag && otherGestureRecognizer == self.scrollView.panGestureRecognizer) { + UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer; + CGPoint velocity = [p velocityInView:self]; + if (self.currentIndex == 0 && velocity.x > 0) { diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 97612c9ecf..e1d5e76aaf 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -483,9 +483,21 @@ function navigate( params?: AllNavigatorParams[K], ) { if (navigationRef.isReady()) { - // @ts-ignore I dont know what would make typescript happy but I have a life -prf - navigationRef.navigate(name, params) + return Promise.race([ + new Promise(resolve => { + const handler = () => { + resolve() + navigationRef.removeListener('state', handler) + } + navigationRef.addListener('state', handler) + + // @ts-ignore I dont know what would make typescript happy but I have a life -prf + navigationRef.navigate(name, params) + }), + timeout(1e3), + ]) } + return Promise.resolve() } function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { diff --git a/src/lib/hooks/useMinimalShellMode.tsx b/src/lib/hooks/useMinimalShellMode.tsx index 68f405dc4d..475d165d3f 100644 --- a/src/lib/hooks/useMinimalShellMode.tsx +++ b/src/lib/hooks/useMinimalShellMode.tsx @@ -1,36 +1,60 @@ import React from 'react' import {autorun} from 'mobx' import {useStores} from 'state/index' -import {Animated} from 'react-native' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import { + Easing, + interpolate, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' export function useMinimalShellMode() { const store = useStores() - const minimalShellInterp = useAnimatedValue(0) - const footerMinimalShellTransform = { - opacity: Animated.subtract(1, minimalShellInterp), - transform: [{translateY: Animated.multiply(minimalShellInterp, 50)}], - } + const minimalShellInterp = useSharedValue(0) + const footerMinimalShellTransform = useAnimatedStyle(() => { + return { + opacity: interpolate(minimalShellInterp.value, [0, 1], [1, 0]), + transform: [ + {translateY: interpolate(minimalShellInterp.value, [0, 1], [0, 25])}, + ], + } + }) + const headerMinimalShellTransform = useAnimatedStyle(() => { + return { + opacity: interpolate(minimalShellInterp.value, [0, 1], [1, 0]), + transform: [ + {translateY: interpolate(minimalShellInterp.value, [0, 1], [0, -25])}, + ], + } + }) + const fabMinimalShellTransform = useAnimatedStyle(() => { + return { + transform: [ + {translateY: interpolate(minimalShellInterp.value, [0, 1], [-44, 0])}, + ], + } + }) React.useEffect(() => { return autorun(() => { if (store.shell.minimalShellMode) { - Animated.timing(minimalShellInterp, { - toValue: 1, - duration: 150, - useNativeDriver: true, - isInteraction: false, - }).start() + minimalShellInterp.value = withTiming(1, { + duration: 125, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }) } else { - Animated.timing(minimalShellInterp, { - toValue: 0, - duration: 150, - useNativeDriver: true, - isInteraction: false, - }).start() + minimalShellInterp.value = withTiming(0, { + duration: 125, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }) } }) - }, [minimalShellInterp, store]) + }, [minimalShellInterp, store.shell.minimalShellMode]) - return {footerMinimalShellTransform} + return { + footerMinimalShellTransform, + headerMinimalShellTransform, + fabMinimalShellTransform, + } } diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx new file mode 100644 index 0000000000..725106d594 --- /dev/null +++ b/src/view/com/feeds/FeedPage.tsx @@ -0,0 +1,210 @@ +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useIsFocused} from '@react-navigation/native' +import {useAnalytics} from '@segment/analytics-react-native' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {ComposeIcon2} from 'lib/icons' +import {colors, s} from 'lib/styles' +import {observer} from 'mobx-react-lite' +import React from 'react' +import {FlatList, View} from 'react-native' +import {useStores} from 'state/index' +import {PostsFeedModel} from 'state/models/feeds/posts' +import {useHeaderOffset, POLL_FREQ} from 'view/screens/Home' +import {Feed} from '../posts/Feed' +import {TextLink} from '../util/Link' +import {FAB} from '../util/fab/FAB' +import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' +import useAppState from 'react-native-appstate-hook' + +export const FeedPage = observer(function FeedPageImpl({ + testID, + isPageFocused, + feed, + renderEmptyState, + renderEndOfFeed, +}: { + testID?: string + feed: PostsFeedModel + isPageFocused: boolean + renderEmptyState: () => JSX.Element + renderEndOfFeed?: () => JSX.Element +}) { + const store = useStores() + const pal = usePalette('default') + const {isDesktop} = useWebMediaQueries() + const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll(store) + const {screen, track} = useAnalytics() + const headerOffset = useHeaderOffset() + const scrollElRef = React.useRef(null) + const {appState} = useAppState({ + onForeground: () => doPoll(true), + }) + const isScreenFocused = useIsFocused() + const hasNew = feed.hasNewLatest && !feed.isRefreshing + + React.useEffect(() => { + // called on first load + if (!feed.hasLoaded && isPageFocused) { + feed.setup() + } + }, [isPageFocused, feed]) + + const doPoll = React.useCallback( + (knownActive = false) => { + if ( + (!knownActive && appState !== 'active') || + !isScreenFocused || + !isPageFocused + ) { + return + } + if (feed.isLoading) { + return + } + store.log.debug('HomeScreen: Polling for new posts') + feed.checkForLatest() + }, + [appState, isScreenFocused, isPageFocused, store, feed], + ) + + const scrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: -headerOffset}) + resetMainScroll() + }, [headerOffset, resetMainScroll]) + + const onSoftReset = React.useCallback(() => { + if (isPageFocused) { + scrollToTop() + feed.refresh() + } + }, [isPageFocused, scrollToTop, feed]) + + // fires when page within screen is activated/deactivated + // - check for latest + React.useEffect(() => { + if (!isPageFocused || !isScreenFocused) { + return + } + + const softResetSub = store.onScreenSoftReset(onSoftReset) + const feedCleanup = feed.registerListeners() + const pollInterval = setInterval(doPoll, POLL_FREQ) + + screen('Feed') + store.log.debug('HomeScreen: Updating feed') + feed.checkForLatest() + + return () => { + clearInterval(pollInterval) + softResetSub.remove() + feedCleanup() + } + }, [store, doPoll, onSoftReset, screen, feed, isPageFocused, isScreenFocused]) + + const onPressCompose = React.useCallback(() => { + track('HomeScreen:PressCompose') + store.shell.openComposer({}) + }, [store, track]) + + const onPressTryAgain = React.useCallback(() => { + feed.refresh() + }, [feed]) + + const onPressLoadLatest = React.useCallback(() => { + scrollToTop() + feed.refresh() + }, [feed, scrollToTop]) + + const ListHeaderComponent = React.useCallback(() => { + if (isDesktop) { + return ( + + + {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} + {hasNew && ( + + )} + + } + onPress={() => store.emitScreenSoftReset()} + /> + + } + /> + + ) + } + return <> + }, [isDesktop, pal, store, hasNew]) + + return ( + + + {(isScrolledDown || hasNew) && ( + + )} + } + accessibilityRole="button" + accessibilityLabel="New post" + accessibilityHint="" + /> + + ) +}) diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 513524864e..7c7ad06163 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -34,11 +34,13 @@ const initialTransform = createTransform() type Props = { imageSrc: ImageSource onRequestClose: () => void + onTap: () => void onZoom: (isZoomed: boolean) => void isScrollViewBeingDragged: boolean } const ImageItem = ({ imageSrc, + onTap, onZoom, onRequestClose, isScrollViewBeingDragged, @@ -227,6 +229,10 @@ const ImageItem = ({ panTranslation.value = {x: 0, y: 0} }) + const singleTap = Gesture.Tap().onEnd(() => { + runOnJS(onTap)() + }) + const doubleTap = Gesture.Tap() .numberOfTaps(2) .onEnd(e => { @@ -297,6 +303,7 @@ const ImageItem = ({ dismissSwipePan, Gesture.Simultaneous(pinch, pan), doubleTap, + singleTap, ) const isLoading = !isLoaded || !imageDimensions diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index cd550670c1..f73f355ac3 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -6,16 +6,9 @@ * */ -import React, {useCallback, useState} from 'react' +import React, {useState} from 'react' -import { - Dimensions, - StyleSheet, - View, - NativeSyntheticEvent, - NativeTouchEvent, - TouchableWithoutFeedback, -} from 'react-native' +import {Dimensions, StyleSheet} from 'react-native' import {Image} from 'expo-image' import Animated, { interpolate, @@ -25,13 +18,13 @@ import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' +import {Gesture, GestureDetector} from 'react-native-gesture-handler' import useImageDimensions from '../../hooks/useImageDimensions' import {ImageSource, Dimensions as ImageDimensions} from '../../@types' import {ImageLoading} from './ImageLoading' -const DOUBLE_TAP_DELAY = 300 const SWIPE_CLOSE_OFFSET = 75 const SWIPE_CLOSE_VELOCITY = 1 const SCREEN = Dimensions.get('screen') @@ -41,15 +34,14 @@ const MIN_DOUBLE_TAP_SCALE = 2 type Props = { imageSrc: ImageSource onRequestClose: () => void + onTap: () => void onZoom: (scaled: boolean) => void isScrollViewBeingDragged: boolean } const AnimatedImage = Animated.createAnimatedComponent(Image) -let lastTapTS: number | null = null - -const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => { +const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => { const scrollViewRef = useAnimatedRef() const translationY = useSharedValue(0) const [loaded, setLoaded] = useState(false) @@ -71,12 +63,18 @@ const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => { const scrollHandler = useAnimatedScrollHandler({ onScroll(e) { - translationY.value = e.zoomScale > 1 ? 0 : e.contentOffset.y + const nextIsScaled = e.zoomScale > 1 + translationY.value = nextIsScaled ? 0 : e.contentOffset.y + if (scaled !== nextIsScaled) { + runOnJS(handleZoom)(nextIsScaled) + } }, onEndDrag(e) { const velocityY = e.velocity?.y ?? 0 const nextIsScaled = e.zoomScale > 1 - runOnJS(handleZoom)(nextIsScaled) + if (scaled !== nextIsScaled) { + runOnJS(handleZoom)(nextIsScaled) + } if (!nextIsScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) { runOnJS(onRequestClose)() } @@ -88,43 +86,46 @@ const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => { setScaled(nextIsScaled) } - const handleDoubleTap = useCallback( - (event: NativeSyntheticEvent) => { - const nowTS = new Date().getTime() - const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() + function handleDoubleTap(absoluteX: number, absoluteY: number) { + const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() + let nextZoomRect = { + x: 0, + y: 0, + width: SCREEN.width, + height: SCREEN.height, + } - if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) { - let nextZoomRect = { - x: 0, - y: 0, - width: SCREEN.width, - height: SCREEN.height, - } + const willZoom = !scaled + if (willZoom) { + nextZoomRect = getZoomRectAfterDoubleTap( + imageDimensions, + absoluteX, + absoluteY, + ) + } - const willZoom = !scaled - if (willZoom) { - const {pageX, pageY} = event.nativeEvent - nextZoomRect = getZoomRectAfterDoubleTap( - imageDimensions, - pageX, - pageY, - ) - } + // @ts-ignore + scrollResponderRef?.scrollResponderZoomTo({ + ...nextZoomRect, // This rect is in screen coordinates + animated: true, + }) + } - // @ts-ignore - scrollResponderRef?.scrollResponderZoomTo({ - ...nextZoomRect, // This rect is in screen coordinates - animated: true, - }) - } else { - lastTapTS = nowTS - } - }, - [imageDimensions, scaled, scrollViewRef], - ) + const singleTap = Gesture.Tap().onEnd(() => { + runOnJS(onTap)() + }) + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .onEnd(e => { + const {absoluteX, absoluteY} = e + runOnJS(handleDoubleTap)(absoluteX, absoluteY) + }) + + const composedGesture = Gesture.Exclusive(doubleTap, singleTap) return ( - + { contentContainerStyle={styles.imageScrollContainer} onScroll={scrollHandler}> {(!loaded || !imageDimensions) && } - - setLoaded(true)} - /> - + accessibilityHint="" + onLoad={() => setLoaded(true)} + /> - + ) } diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx index 35be96e46c..16688b820e 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx @@ -7,6 +7,7 @@ import {ImageSource} from '../../@types' type Props = { imageSrc: ImageSource onRequestClose: () => void + onTap: () => void onZoom: (scaled: boolean) => void isScrollViewBeingDragged: boolean } diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 78d16f8a62..b6835793d1 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -43,24 +43,36 @@ function ImageViewing({ const [isScaled, setIsScaled] = useState(false) const [isDragging, setIsDragging] = useState(false) const [imageIndex, setImageIndex] = useState(initialImageIndex) + const [showControls, setShowControls] = useState(true) const animatedHeaderStyle = useAnimatedStyle(() => ({ + pointerEvents: showControls ? 'auto' : 'none', + opacity: withClampedSpring(showControls ? 1 : 0), transform: [ { - translateY: withClampedSpring(isScaled ? -300 : 0), + translateY: withClampedSpring(showControls ? 0 : -30), }, ], })) const animatedFooterStyle = useAnimatedStyle(() => ({ + pointerEvents: showControls ? 'auto' : 'none', + opacity: withClampedSpring(showControls ? 1 : 0), transform: [ { - translateY: withClampedSpring(isScaled ? 300 : 0), + translateY: withClampedSpring(showControls ? 0 : 30), }, ], })) + const onTap = useCallback(() => { + setShowControls(show => !show) + }, []) + const onZoom = useCallback((nextIsScaled: boolean) => { setIsScaled(nextIsScaled) + if (nextIsScaled) { + setShowControls(false) + } }, []) const edges = useMemo(() => { @@ -105,6 +117,7 @@ function ImageViewing({ {images.map(imageSrc => ( ( function withClampedSpring(value: any) { 'worklet' - return withSpring(value, {overshootClamping: true}) + return withSpring(value, {overshootClamping: true, stiffness: 300}) } export default EnhancedImageViewing diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 4f3f424a31..1fe1299d73 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -1,11 +1,12 @@ import React, {useRef, useEffect} from 'react' import {StyleSheet} from 'react-native' -import {SafeAreaView} from 'react-native-safe-area-context' +import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' import {useStores} from 'state/index' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {usePalette} from 'lib/hooks/usePalette' +import {timeout} from 'lib/async/timeout' import {navigate} from '../../../Navigation' import once from 'lodash.once' @@ -36,11 +37,13 @@ import * as SwitchAccountModal from './SwitchAccount' import * as LinkWarningModal from './LinkWarning' const DEFAULT_SNAPPOINTS = ['90%'] +const HANDLE_HEIGHT = 24 export const ModalsContainer = observer(function ModalsContainer() { const store = useStores() const bottomSheetRef = useRef(null) const pal = usePalette('default') + const safeAreaInsets = useSafeAreaInsets() const activeModal = store.shell.activeModals[store.shell.activeModals.length - 1] @@ -53,12 +56,16 @@ export const ModalsContainer = observer(function ModalsContainer() { navigateOnce('Profile', {name: activeModal.did}) } } - const onBottomSheetChange = (snapPoint: number) => { + const onBottomSheetChange = async (snapPoint: number) => { if (snapPoint === -1) { store.shell.closeModal() } else if (activeModal?.name === 'profile-preview' && snapPoint === 1) { - // ensure we navigate to Profile and close the modal - navigateOnce('Profile', {name: activeModal.did}) + await navigateOnce('Profile', {name: activeModal.did}) + // There is no particular callback for when the view has actually been presented. + // This delay gives us a decent chance the navigation has flushed *and* images have loaded. + // It's acceptable because the data is already being fetched + it usually takes longer anyway. + // TODO: Figure out why avatar/cover don't always show instantly from cache. + await timeout(200) store.shell.closeModal() } } @@ -75,6 +82,7 @@ export const ModalsContainer = observer(function ModalsContainer() { } }, [store.shell.isModalActive, bottomSheetRef, activeModal?.name]) + let needsSafeTopInset = false let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS let element if (activeModal?.name === 'confirm') { @@ -86,6 +94,7 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'profile-preview') { snapPoints = ProfilePreviewModal.snapPoints element = + needsSafeTopInset = true // Need to align with the target profile screen. } else if (activeModal?.name === 'server-input') { snapPoints = ServerInputModal.snapPoints element = @@ -164,10 +173,13 @@ export const ModalsContainer = observer(function ModalsContainer() { ) } + const topInset = needsSafeTopInset ? safeAreaInsets.top - HANDLE_HEIGHT : 0 return ( - + void}, @@ -31,26 +32,12 @@ const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( [store.me.savedFeeds.pinnedFeedNames], ) const pal = usePalette('default') - const interp = useAnimatedValue(0) - - React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 1 : 0, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) - const transform = { - transform: [ - {translateX: '-50%'}, - {translateY: Animated.multiply(interp, -100)}, - ], - } + const {headerMinimalShellTransform} = useMinimalShellMode() return ( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf - + void}, ) { const store = useStores() const pal = usePalette('default') - const interp = useAnimatedValue(0) - - React.useEffect(() => { - return autorun(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 1 : 0, - duration: 150, - useNativeDriver: true, - isInteraction: false, - }).start() - }) - }, [interp, store]) - const transform = { - opacity: Animated.subtract(1, interp), - transform: [{translateY: Animated.multiply(interp, -50)}], - } const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) + const {headerMinimalShellTransform} = useMinimalShellMode() const onPressAvi = React.useCallback(() => { store.shell.openDrawer() @@ -48,13 +33,17 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( [store.me.savedFeeds.pinnedFeedNames], ) + const tabBarKey = useMemo(() => { + return items.join(',') + }, [items]) + return ( @@ -92,8 +81,11 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 319d28f954..8614bdf64d 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -64,6 +64,7 @@ export function TabBar({ ) const styles = isDesktop || isTablet ? desktopStyles : mobileStyles + return ( { - if (!feed.hasLoaded) return + if (!feed.hasLoaded || !feed.hasMore) return track('Feed:onEndReached') try { @@ -178,7 +178,7 @@ export const Feed = observer(function Feed({ scrollEventThrottle={scrollEventThrottle} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} - onEndReachedThreshold={0.6} + onEndReachedThreshold={2} removeClippedSubviews={true} contentOffset={{x: 0, y: headerOffset * -1}} extraData={extraData} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index baf95af6cc..df19ecad57 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -60,14 +60,14 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({ if (!view || !view.hasLoaded) { return ( - + - + diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index 41e4022d59..c5b187fb3d 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -223,9 +223,9 @@ const SuggestedFollow = observer(function SuggestedFollowImpl({ const onPress = React.useCallback(async () => { try { - const {following} = await toggle() + const {following: isFollowing} = await toggle() - if (following) { + if (isFollowing) { track('ProfileHeader:SuggestedFollowFollowed') } } catch (e: any) { diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 3a34777ab9..ec459b4ebf 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,17 +1,17 @@ import React from 'react' import {observer} from 'mobx-react-lite' -import {autorun} from 'mobx' -import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import {CenteredView} from './Views' import {Text} from './text/Text' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useAnalytics} from 'lib/analytics/analytics' import {NavigationProp} from 'lib/routes/types' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} @@ -150,32 +150,8 @@ const Container = observer(function ContainerImpl({ hideOnScroll: boolean showBorder?: boolean }) { - const store = useStores() const pal = usePalette('default') - const interp = useAnimatedValue(0) - - React.useEffect(() => { - return autorun(() => { - if (store.shell.minimalShellMode) { - Animated.timing(interp, { - toValue: 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } else { - Animated.timing(interp, { - toValue: 0, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } - }) - }, [interp, store]) - const transform = { - transform: [{translateY: Animated.multiply(interp, -100)}], - } + const {headerMinimalShellTransform} = useMinimalShellMode() if (!hideOnScroll) { return ( @@ -198,7 +174,7 @@ const Container = observer(function ContainerImpl({ styles.headerFloating, pal.view, pal.border, - transform, + headerMinimalShellTransform, showBorder && styles.border, ]}> {children} diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 97eeba358a..5b1d5d8883 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -1,14 +1,13 @@ import React, {ComponentProps} from 'react' import {observer} from 'mobx-react-lite' -import {autorun} from 'mobx' -import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native' +import {StyleSheet, TouchableWithoutFeedback} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {gradients} from 'lib/styles' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {clamp} from 'lib/numbers' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' export interface FABProps extends ComponentProps { @@ -22,30 +21,30 @@ export const FABInner = observer(function FABInnerImpl({ ...props }: FABProps) { const insets = useSafeAreaInsets() - const {isTablet} = useWebMediaQueries() - const store = useStores() - const interp = useAnimatedValue(0) - React.useEffect(() => { - return autorun(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 0 : 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }) - }, [interp, store]) - const transform = isTablet - ? undefined - : { - transform: [{translateY: Animated.multiply(interp, -44)}], - } - const size = isTablet ? styles.sizeLarge : styles.sizeRegular - const right = isTablet ? 50 : 24 - const bottom = isTablet ? 50 : clamp(insets.bottom, 15, 60) + 15 + const {isMobile, isTablet} = useWebMediaQueries() + const {fabMinimalShellTransform} = useMinimalShellMode() + + const size = React.useMemo(() => { + return isTablet ? styles.sizeLarge : styles.sizeRegular + }, [isTablet]) + const tabletSpacing = React.useMemo(() => { + return isTablet + ? {right: 50, bottom: 50} + : { + right: 24, + bottom: clamp(insets.bottom, 15, 60) + 15, + } + }, [insets.bottom, isTablet]) + return ( - + - + <> + @@ -72,7 +72,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { - + @@ -80,7 +80,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { - + ) default: diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index 57c3baa5b2..b16a423964 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -2,16 +2,12 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {colors} from 'lib/styles' import {HITSLOP_20} from 'lib/constants' -import {isWeb} from 'platform/detection' -import {clamp} from 'lib/numbers' -import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated' - +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) @@ -23,20 +19,11 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ onPress: () => void label: string showIndicator: boolean - minimalShellMode?: boolean // NOTE not used on mobile -prf }) { - const store = useStores() const pal = usePalette('default') - const {isDesktop, isTablet} = useWebMediaQueries() - const safeAreaInsets = useSafeAreaInsets() - const minMode = store.shell.minimalShellMode - const bottom = isTablet - ? 50 - : (minMode || isDesktop ? 16 : 60) + - (isWeb ? 20 : clamp(safeAreaInsets.bottom, 15, 60)) - const animatedStyle = useAnimatedStyle(() => ({ - bottom: withTiming(bottom, {duration: 150}), - })) + const {isDesktop, isTablet, isMobile} = useWebMediaQueries() + const {fabMinimalShellTransform} = useMinimalShellMode() + return ( export const HomeScreen = withAuthRequired( @@ -98,7 +87,9 @@ export const HomeScreen = withAuthRequired( (props: RenderTabBarFnProps) => { return ( @@ -111,10 +102,6 @@ export const HomeScreen = withAuthRequired( return }, []) - const renderFollowingEndOfFeed = React.useCallback(() => { - return - }, []) - const renderCustomFeedEmptyState = React.useCallback(() => { return }, []) @@ -132,7 +119,7 @@ export const HomeScreen = withAuthRequired( isPageFocused={selectedPage === 0} feed={store.me.mainFeed} renderEmptyState={renderFollowingEmptyState} - renderEndOfFeed={renderFollowingEndOfFeed} + renderEndOfFeed={FollowingEndOfFeed} /> {customFeeds.map((f, index) => { return ( @@ -150,196 +137,7 @@ export const HomeScreen = withAuthRequired( }), ) -const FeedPage = observer(function FeedPageImpl({ - testID, - isPageFocused, - feed, - renderEmptyState, - renderEndOfFeed, -}: { - testID?: string - feed: PostsFeedModel - isPageFocused: boolean - renderEmptyState: () => JSX.Element - renderEndOfFeed?: () => JSX.Element -}) { - const store = useStores() - const pal = usePalette('default') - const {isDesktop} = useWebMediaQueries() - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll(store) - const {screen, track} = useAnalytics() - const headerOffset = useHeaderOffset() - const scrollElRef = React.useRef(null) - const {appState} = useAppState({ - onForeground: () => doPoll(true), - }) - const isScreenFocused = useIsFocused() - const hasNew = feed.hasNewLatest && !feed.isRefreshing - - React.useEffect(() => { - // called on first load - if (!feed.hasLoaded && isPageFocused) { - feed.setup() - } - }, [isPageFocused, feed]) - - const doPoll = React.useCallback( - (knownActive = false) => { - if ( - (!knownActive && appState !== 'active') || - !isScreenFocused || - !isPageFocused - ) { - return - } - if (feed.isLoading) { - return - } - store.log.debug('HomeScreen: Polling for new posts') - feed.checkForLatest() - }, - [appState, isScreenFocused, isPageFocused, store, feed], - ) - - const scrollToTop = React.useCallback(() => { - scrollElRef.current?.scrollToOffset({offset: -headerOffset}) - resetMainScroll() - }, [headerOffset, resetMainScroll]) - - const onSoftReset = React.useCallback(() => { - if (isPageFocused) { - scrollToTop() - feed.refresh() - } - }, [isPageFocused, scrollToTop, feed]) - - // fires when page within screen is activated/deactivated - // - check for latest - React.useEffect(() => { - if (!isPageFocused || !isScreenFocused) { - return - } - - const softResetSub = store.onScreenSoftReset(onSoftReset) - const feedCleanup = feed.registerListeners() - const pollInterval = setInterval(doPoll, POLL_FREQ) - - screen('Feed') - store.log.debug('HomeScreen: Updating feed') - feed.checkForLatest() - - return () => { - clearInterval(pollInterval) - softResetSub.remove() - feedCleanup() - } - }, [store, doPoll, onSoftReset, screen, feed, isPageFocused, isScreenFocused]) - - const onPressCompose = React.useCallback(() => { - track('HomeScreen:PressCompose') - store.shell.openComposer({}) - }, [store, track]) - - const onPressTryAgain = React.useCallback(() => { - feed.refresh() - }, [feed]) - - const onPressLoadLatest = React.useCallback(() => { - scrollToTop() - feed.refresh() - }, [feed, scrollToTop]) - - const ListHeaderComponent = React.useCallback(() => { - if (isDesktop) { - return ( - - - {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} - {hasNew && ( - - )} - - } - onPress={() => store.emitScreenSoftReset()} - /> - - } - /> - - ) - } - return <> - }, [isDesktop, pal, store, hasNew]) - - return ( - - - {(isScrolledDown || hasNew) && ( - - )} - } - accessibilityRole="button" - accessibilityLabel="New post" - accessibilityHint="" - /> - - ) -}) - -function useHeaderOffset() { +export function useHeaderOffset() { const {isDesktop, isTablet} = useWebMediaQueries() const {fontScale} = useWindowDimensions() if (isDesktop) { diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 9774013503..b00bfb7657 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -156,7 +156,6 @@ export const NotificationsScreen = withAuthRequired( onPress={onPressLoadLatest} label="Load new notifications" showIndicator={hasNew} - minimalShellMode={true} /> )} diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 984aef25d9..cfd4d46d01 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -1,10 +1,6 @@ import React, {ComponentProps} from 'react' -import { - Animated, - GestureResponderEvent, - TouchableOpacity, - View, -} from 'react-native' +import {GestureResponderEvent, TouchableOpacity, View} from 'react-native' +import Animated from 'react-native-reanimated' import {StackActions} from '@react-navigation/native' import {BottomTabBarProps} from '@react-navigation/bottom-tabs' import {useSafeAreaInsets} from 'react-native-safe-area-context' diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index e20214235f..ebcc527a12 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -2,8 +2,8 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {Animated} from 'react-native' import {useNavigationState} from '@react-navigation/native' +import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {getCurrentRoute, isTab} from 'lib/routes/helpers' import {styles} from './BottomBarStyles' diff --git a/yarn.lock b/yarn.lock index 5ae15bfd22..59c86b04d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3754,6 +3754,16 @@ "@sentry/utils" "7.52.1" tslib "^1.9.3" +"@sentry-internal/tracing@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.69.0.tgz#8d8eb740b72967b6ba3fdc0a5173aa55331b7d35" + integrity sha512-4BgeWZUj9MO6IgfO93C9ocP3+AdngqujF/+zB2rFdUe+y9S6koDyUC7jr9Knds/0Ta72N/0D6PwhgSCpHK8s0Q== + dependencies: + "@sentry/core" "7.69.0" + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + tslib "^2.4.1 || ^1.9.3" + "@sentry/browser@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.52.0.tgz#55d266c89ed668389ff687e5cc885c27016ea85c" @@ -3778,6 +3788,18 @@ "@sentry/utils" "7.52.1" tslib "^1.9.3" +"@sentry/browser@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.69.0.tgz#65427c90fb71c1775e2c1e38431efb7f4aec1e34" + integrity sha512-5ls+zu2PrMhHCIIhclKQsWX5u6WH0Ez5/GgrCMZTtZ1d70ukGSRUvpZG9qGf5Cw1ezS1LY+1HCc3whf8x8lyPw== + dependencies: + "@sentry-internal/tracing" "7.69.0" + "@sentry/core" "7.69.0" + "@sentry/replay" "7.69.0" + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + tslib "^2.4.1 || ^1.9.3" + "@sentry/cli@2.17.5": version "2.17.5" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.17.5.tgz#d41e24893a843bcd41e14274044a7ddea9332824" @@ -3789,6 +3811,17 @@ proxy-from-env "^1.1.0" which "^2.0.2" +"@sentry/cli@2.20.7": + version "2.20.7" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.20.7.tgz#8f7f3f632c330cac6bd2278d820948163f3128a6" + integrity sha512-YaHKEUdsFt59nD8yLvuEGCOZ3/ArirL8GZ/66RkZ8wcD2wbpzOFbzo08Kz4te/Eo3OD5/RdW+1dPaOBgGbrXlA== + dependencies: + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + "@sentry/core@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.52.0.tgz#6c820ca48fe2f06bfd6b290044c96de2375f2ad4" @@ -3807,6 +3840,15 @@ "@sentry/utils" "7.52.1" tslib "^1.9.3" +"@sentry/core@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.69.0.tgz#ebbe01df573f438f8613107020a4e18eb9adca4d" + integrity sha512-V6jvK2lS8bhqZDMFUtvwe2XvNstFQf5A+2LMKCNBOV/NN6eSAAd6THwEpginabjet9dHsNRmMk7WNKvrUfQhZw== + dependencies: + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + tslib "^2.4.1 || ^1.9.3" + "@sentry/hub@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.52.0.tgz#ffc087d58c745d57108862faa0f701b15503dcc2" @@ -3817,6 +3859,16 @@ "@sentry/utils" "7.52.0" tslib "^1.9.3" +"@sentry/hub@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.69.0.tgz#3ef3b98e1810b05cb4fb37a861bd700ef592a2a9" + integrity sha512-71TQ7P5de9+cdW1ETGI9wgi2VNqfyWaM3cnUvheXaSjPRBrr6mhwoaSjo+GGsiwx97Ob9DESZEIhdzcLupzkFA== + dependencies: + "@sentry/core" "7.69.0" + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + tslib "^2.4.1 || ^1.9.3" + "@sentry/integrations@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.52.0.tgz#632aa5e54bdfdab910a24057c2072634a2670409" @@ -3837,6 +3889,30 @@ localforage "^1.8.1" tslib "^1.9.3" +"@sentry/integrations@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.69.0.tgz#04c0206d9436ec7b79971e3bde5d6e1e9194595f" + integrity sha512-FEFtFqXuCo9+L7bENZxFpEAlIODwHl6FyW/DwLfniy9jOXHU7BhP/oICLrFE5J7rh1gNY7N/8VlaiQr3hCnS/g== + dependencies: + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + localforage "^1.8.1" + tslib "^2.4.1 || ^1.9.3" + +"@sentry/react-native@5.10.0": + version "5.10.0" + resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-5.10.0.tgz#b61861276fcb35e69dbe9c4e098ed7c88598f5d9" + integrity sha512-YuEZJ3tW5qZlFGFm2FoAZ9vw1fWnjrhMh1IHxo+nUHP3FvVgGkAd/PmSSbgPr2T3YLOIJNiyDdG031Qi7YvtGA== + dependencies: + "@sentry/browser" "7.69.0" + "@sentry/cli" "2.20.7" + "@sentry/core" "7.69.0" + "@sentry/hub" "7.69.0" + "@sentry/integrations" "7.69.0" + "@sentry/react" "7.69.0" + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + "@sentry/react-native@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-5.5.0.tgz#b1283f68465b1772ad6059ebba149673cef33f2d" @@ -3873,6 +3949,17 @@ hoist-non-react-statics "^3.3.2" tslib "^1.9.3" +"@sentry/react@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.69.0.tgz#b9931ac590d8dad3390a9a03a516f1b1bd75615e" + integrity sha512-J+DciRRVuruf1nMmBOi2VeJkOLGeCb4vTOFmHzWTvRJNByZ0flyo8E/fyROL7+23kBq1YbcVY6IloUlH73hneQ== + dependencies: + "@sentry/browser" "7.69.0" + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + hoist-non-react-statics "^3.3.2" + tslib "^2.4.1 || ^1.9.3" + "@sentry/replay@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.52.0.tgz#4d78e88282d2c1044ea4b648a68d1b22173e810d" @@ -3891,6 +3978,15 @@ "@sentry/types" "7.52.1" "@sentry/utils" "7.52.1" +"@sentry/replay@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.69.0.tgz#d727f96292d2b7c25df022fa53764fd39910fcda" + integrity sha512-oUqWyBPFUgShdVvgJtV65EQH9pVDmoYVQMOu59JI6FHVeL3ald7R5Mvz6GaNLXsirvvhp0yAkcAd2hc5Xi6hDw== + dependencies: + "@sentry/core" "7.69.0" + "@sentry/types" "7.69.0" + "@sentry/utils" "7.69.0" + "@sentry/types@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.52.0.tgz#b7d5372f17355e3991cbe818ad567f3fe277cc6b" @@ -3901,6 +3997,11 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.52.1.tgz#bcff6d0462d9b9b7b9ec31c0068fe02d44f25da2" integrity sha512-OMbGBPrJsw0iEXwZ2bJUYxewI1IEAU2e1aQGc0O6QW5+6hhCh+8HO8Xl4EymqwejjztuwStkl6G1qhK+Q0/Row== +"@sentry/types@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.69.0.tgz#012b8d90d270a473cc2a5cf58a56870542739292" + integrity sha512-zPyCox0mzitzU6SIa1KIbNoJAInYDdUpdiA+PoUmMn2hFMH1llGU/cS7f4w/mAsssTlbtlBi72RMnWUCy578bw== + "@sentry/utils@7.52.0": version "7.52.0" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.52.0.tgz#cacc36d905036ba7084c14965e964fc44239d7f0" @@ -3917,6 +4018,14 @@ "@sentry/types" "7.52.1" tslib "^1.9.3" +"@sentry/utils@7.69.0": + version "7.69.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.69.0.tgz#b7594e4eb2a88b9b25298770b841dd3f81bd2aa4" + integrity sha512-4eBixe5Y+0EGVU95R4NxH3jkkjtkE4/CmSZD4In8SCkWGSauogePtq6hyiLsZuP1QHdpPb9Kt0+zYiBb2LouBA== + dependencies: + "@sentry/types" "7.69.0" + tslib "^2.4.1 || ^1.9.3" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -6542,6 +6651,11 @@ babel-plugin-transform-react-remove-prop-types@^0.4.24: resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== +babel-plugin-transform-remove-console@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780" + integrity sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg== + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -16726,7 +16840,7 @@ send@0.18.0, send@^0.18.0: range-parser "~1.2.1" statuses "2.0.1" -sentry-expo@~7.0.0: +sentry-expo@~7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/sentry-expo/-/sentry-expo-7.0.1.tgz#025f0e90ab7f7cba1e00c892fabc027de21bc5bc" integrity sha512-8vmOy4R+qM1peQA9EP8rDGUMBhgMU1D5FyuWY9kfNGatmWuvEmlZpVgaXoXaNPIhPgf2TMrvQIlbqLHtTkoeSA== @@ -17917,7 +18031,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, "tslib@^2.4.1 || ^1.9.3": version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==