Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Performance optimization (#1676)

* upgrade sentry to support profiling monitoring

* remove console logs in production builds

* feeds tab bar and bottom bar animation centralized

* refactor FeedPage out of Home

* add script to start in production mode

* move FAB inner to reanimated

* move FABInner back to `Animated` RN animation

* add perf commands

* add testing with Maestro and perf with Flashlight

* fix merge conflicts

* fix resourceClass name in eas.json

* fix onEndReachedThreshold in Feed

* memoize styles

* go back to old styling for LoadLatestBtn

* remove reanimated code from useMinimalShellMode

* move shell animations to hook/reanimated for perf

* fix empty state issue

* make shell animation feel smoother

* make shell animation more smooth

* run animation with autorun

* specify keys for tab bar properly

* remove comments

* remove already imported dep

* fix lint

* add testing instructions

* mock sentry-expo for jest

* fix jest mocks

* Fix the load-latest button on desktop and tablet

* Fix: don't move the FAB in tablet mode

* Fix type error

* Fix tabs bar positioning on tablet

* Fix types

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by

Ansh
Paul Frazee
and committed by
GitHub
8e9cf182 9042f503

+584 -374
+4 -1
.gitignore
··· 99 99 .env.* 100 100 101 101 # Firebase (Android) Google services 102 - google-services.json 102 + google-services.json 103 + 104 + # Performance results (Flashlight) 105 + .perf/
+77
__e2e__/maestro/scroll.yaml
··· 1 + # flow.yaml 2 + 3 + appId: xyz.blueskyweb.app 4 + --- 5 + - launchApp 6 + # Login 7 + # - runFlow: 8 + # when: 9 + # - tapOn: "Sign In" 10 + # - tapOn: "Username or email address" 11 + # - inputText: "ansh.bsky.team" 12 + # - tapOn: "Password" 13 + # - inputText: "PASSWORd" 14 + # - tapOn: "Next" 15 + # Allow notifications if popup is visible 16 + # - runFlow: 17 + # when: 18 + # visible: "Notifications" 19 + # commands: 20 + # - tapOn: "Allow" 21 + # Scroll in main feed 22 + - "scroll" 23 + - "scroll" 24 + - "scroll" 25 + - "scroll" 26 + - "scroll" 27 + - "scroll" 28 + - "scroll" 29 + - "scroll" 30 + # Swipe between feeds 31 + - swipe: 32 + direction: "LEFT" 33 + - swipe: 34 + direction: "LEFT" 35 + - swipe: 36 + direction: "LEFT" 37 + - swipe: 38 + direction: "RIGHT" 39 + - swipe: 40 + direction: "RIGHT" 41 + - swipe: 42 + direction: "RIGHT" 43 + # Go to Notifications 44 + - tapOn: 45 + id: "viewHeaderDrawerBtn" 46 + - tapOn: "Notifications" 47 + - "scroll" 48 + - "scroll" 49 + - "scroll" 50 + - "scroll" 51 + - "scroll" 52 + - swipe: 53 + direction: "DOWN" # Make header visible 54 + # Go to Feeds tab 55 + - tapOn: 56 + id: "viewHeaderDrawerBtn" 57 + - tapOn: "Feeds" 58 + - scrollUntilVisible: 59 + element: "Discover" 60 + direction: UP 61 + - tapOn: "Discover" 62 + - waitForAnimationToEnd 63 + - "scroll" 64 + - "scroll" 65 + - "scroll" 66 + - "scroll" 67 + - "scroll" 68 + # Click on post 69 + - tapOn: 70 + id: "postText" 71 + index: 0 72 + - "scroll" 73 + - "scroll" 74 + - "scroll" 75 + - "scroll" 76 + - "scroll" 77 +
+10
__mocks__/sentry-expo.js
··· 1 + jest.mock('sentry-expo', () => ({ 2 + init: () => jest.fn(), 3 + Native: { 4 + ReactNativeTracing: jest.fn().mockImplementation(() => ({ 5 + start: jest.fn(), 6 + stop: jest.fn(), 7 + })), 8 + ReactNavigationInstrumentation: jest.fn(), 9 + }, 10 + }))
+5
babel.config.js
··· 30 30 ], 31 31 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last 32 32 ], 33 + env: { 34 + production: { 35 + plugins: ['transform-remove-console'], 36 + }, 37 + }, 33 38 } 34 39 }
+14
docs/testing.md
··· 1 + # Testing instructions 2 + 3 + ### Using Maestro E2E tests 4 + 1. Install Maestro by following [these instuctions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests. 5 + 2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one. 6 + 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. 7 + 8 + 9 + ### Using Flashlight for Performance Testing 10 + 1. Make sure Maestro is installed (optional: only for auomated testing) by following the instructions above 11 + 2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/) 12 + 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) 13 + 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` 14 + 5. You can also run your own tests by running `yarn perf:test <path_to_test>` where `<path_to_test>` 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`.
+4 -4
eas.json
··· 9 9 "distribution": "internal", 10 10 "ios": { 11 11 "simulator": true, 12 - "resourceClass": "m-large" 12 + "resourceClass": "large" 13 13 }, 14 14 "channel": "development" 15 15 }, ··· 17 17 "developmentClient": true, 18 18 "distribution": "internal", 19 19 "ios": { 20 - "resourceClass": "m-large" 20 + "resourceClass": "large" 21 21 }, 22 22 "channel": "development" 23 23 }, 24 24 "preview": { 25 25 "distribution": "internal", 26 26 "ios": { 27 - "resourceClass": "m-large" 27 + "resourceClass": "large" 28 28 }, 29 29 "channel": "preview" 30 30 }, 31 31 "production": { 32 32 "ios": { 33 - "resourceClass": "m-large" 33 + "resourceClass": "large" 34 34 }, 35 35 "channel": "production" 36 36 },
+11
jest/jestSetup.js
··· 74 74 __esModule: true, // this property makes it work 75 75 default: jest.fn().mockReturnValue([['eng']]), 76 76 })) 77 + 78 + jest.mock('sentry-expo', () => ({ 79 + init: () => jest.fn(), 80 + Native: { 81 + ReactNativeTracing: jest.fn().mockImplementation(() => ({ 82 + start: jest.fn(), 83 + stop: jest.fn(), 84 + })), 85 + ReactNavigationInstrumentation: jest.fn(), 86 + }, 87 + }))
+9 -2
package.json
··· 11 11 "web": "expo start --web", 12 12 "build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/", 13 13 "start": "expo start --dev-client", 14 + "start:prod": "expo start --dev-client --no-dev --minify", 14 15 "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", 15 16 "test": "jest --forceExit --testTimeout=20000 --bail", 16 17 "test-watch": "jest --watchAll", ··· 22 23 "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 23 24 "e2e:build": "detox build -c ios.sim.debug", 24 25 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", 26 + "perf:test": "maestro test", 27 + "perf:test:run": "maestro test __e2e__/maestro/scroll.yaml", 28 + "perf:test:measure": "flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json", 29 + "perf:test:results": "flashlight report .perf/results.json", 30 + "perf:measure": "flashlight measure", 25 31 "build:apk": "eas build -p android --profile dev-android-apk" 26 32 }, 27 33 "dependencies": { ··· 53 59 "@segment/analytics-react": "^1.0.0-rc1", 54 60 "@segment/analytics-react-native": "^2.10.1", 55 61 "@segment/sovran-react-native": "^0.4.5", 56 - "@sentry/react-native": "5.5.0", 62 + "@sentry/react-native": "5.10.0", 57 63 "@tanstack/react-query": "^4.33.0", 58 64 "@tiptap/core": "^2.0.0-beta.220", 59 65 "@tiptap/extension-document": "^2.0.0-beta.220", ··· 71 77 "@zxing/text-encoding": "^0.9.0", 72 78 "array.prototype.findlast": "^1.2.3", 73 79 "await-lock": "^2.2.2", 80 + "babel-plugin-transform-remove-console": "^6.9.4", 74 81 "base64-js": "^1.5.1", 75 82 "bcp-47-match": "^2.0.3", 76 83 "email-validator": "^2.0.4", ··· 148 155 "react-native-web-linear-gradient": "^1.1.2", 149 156 "react-responsive": "^9.0.2", 150 157 "rn-fetch-blob": "^0.12.0", 151 - "sentry-expo": "~7.0.0", 158 + "sentry-expo": "~7.0.1", 152 159 "tippy.js": "^6.3.7", 153 160 "tlds": "^1.234.0", 154 161 "zeego": "^1.6.2",
+2 -1
patches/@sentry+react-native+5.5.0.patch patches/@sentry+react-native+5.10.0.patch
··· 1 1 diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js 2 - index 7e0b4cd..3fd7406 100644 2 + index 7e0b4cd..177454c 100644 3 3 --- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js 4 4 +++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js 5 5 @@ -3,6 +3,8 @@ import { LogBox } from 'react-native'; ··· 12 12 + } catch (e) {} 13 13 } 14 14 //# sourceMappingURL=ignorerequirecyclelogs.js.map 15 + \ No newline at end of file
+45 -21
src/lib/hooks/useMinimalShellMode.tsx
··· 1 1 import React from 'react' 2 2 import {autorun} from 'mobx' 3 3 import {useStores} from 'state/index' 4 - import {Animated} from 'react-native' 5 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 4 + import { 5 + Easing, 6 + interpolate, 7 + useAnimatedStyle, 8 + useSharedValue, 9 + withTiming, 10 + } from 'react-native-reanimated' 6 11 7 12 export function useMinimalShellMode() { 8 13 const store = useStores() 9 - const minimalShellInterp = useAnimatedValue(0) 10 - const footerMinimalShellTransform = { 11 - opacity: Animated.subtract(1, minimalShellInterp), 12 - transform: [{translateY: Animated.multiply(minimalShellInterp, 50)}], 13 - } 14 + const minimalShellInterp = useSharedValue(0) 15 + const footerMinimalShellTransform = useAnimatedStyle(() => { 16 + return { 17 + opacity: interpolate(minimalShellInterp.value, [0, 1], [1, 0]), 18 + transform: [ 19 + {translateY: interpolate(minimalShellInterp.value, [0, 1], [0, 25])}, 20 + ], 21 + } 22 + }) 23 + const headerMinimalShellTransform = useAnimatedStyle(() => { 24 + return { 25 + opacity: interpolate(minimalShellInterp.value, [0, 1], [1, 0]), 26 + transform: [ 27 + {translateY: interpolate(minimalShellInterp.value, [0, 1], [0, -25])}, 28 + ], 29 + } 30 + }) 31 + const fabMinimalShellTransform = useAnimatedStyle(() => { 32 + return { 33 + transform: [ 34 + {translateY: interpolate(minimalShellInterp.value, [0, 1], [-44, 0])}, 35 + ], 36 + } 37 + }) 14 38 15 39 React.useEffect(() => { 16 40 return autorun(() => { 17 41 if (store.shell.minimalShellMode) { 18 - Animated.timing(minimalShellInterp, { 19 - toValue: 1, 20 - duration: 150, 21 - useNativeDriver: true, 22 - isInteraction: false, 23 - }).start() 42 + minimalShellInterp.value = withTiming(1, { 43 + duration: 125, 44 + easing: Easing.bezier(0.25, 0.1, 0.25, 1), 45 + }) 24 46 } else { 25 - Animated.timing(minimalShellInterp, { 26 - toValue: 0, 27 - duration: 150, 28 - useNativeDriver: true, 29 - isInteraction: false, 30 - }).start() 47 + minimalShellInterp.value = withTiming(0, { 48 + duration: 125, 49 + easing: Easing.bezier(0.25, 0.1, 0.25, 1), 50 + }) 31 51 } 32 52 }) 33 - }, [minimalShellInterp, store]) 53 + }, [minimalShellInterp, store.shell.minimalShellMode]) 34 54 35 - return {footerMinimalShellTransform} 55 + return { 56 + footerMinimalShellTransform, 57 + headerMinimalShellTransform, 58 + fabMinimalShellTransform, 59 + } 36 60 }
+210
src/view/com/feeds/FeedPage.tsx
··· 1 + import { 2 + FontAwesomeIcon, 3 + FontAwesomeIconStyle, 4 + } from '@fortawesome/react-native-fontawesome' 5 + import {useIsFocused} from '@react-navigation/native' 6 + import {useAnalytics} from '@segment/analytics-react-native' 7 + import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 8 + import {usePalette} from 'lib/hooks/usePalette' 9 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 + import {ComposeIcon2} from 'lib/icons' 11 + import {colors, s} from 'lib/styles' 12 + import {observer} from 'mobx-react-lite' 13 + import React from 'react' 14 + import {FlatList, View} from 'react-native' 15 + import {useStores} from 'state/index' 16 + import {PostsFeedModel} from 'state/models/feeds/posts' 17 + import {useHeaderOffset, POLL_FREQ} from 'view/screens/Home' 18 + import {Feed} from '../posts/Feed' 19 + import {TextLink} from '../util/Link' 20 + import {FAB} from '../util/fab/FAB' 21 + import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' 22 + import useAppState from 'react-native-appstate-hook' 23 + 24 + export const FeedPage = observer(function FeedPageImpl({ 25 + testID, 26 + isPageFocused, 27 + feed, 28 + renderEmptyState, 29 + renderEndOfFeed, 30 + }: { 31 + testID?: string 32 + feed: PostsFeedModel 33 + isPageFocused: boolean 34 + renderEmptyState: () => JSX.Element 35 + renderEndOfFeed?: () => JSX.Element 36 + }) { 37 + const store = useStores() 38 + const pal = usePalette('default') 39 + const {isDesktop} = useWebMediaQueries() 40 + const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll(store) 41 + const {screen, track} = useAnalytics() 42 + const headerOffset = useHeaderOffset() 43 + const scrollElRef = React.useRef<FlatList>(null) 44 + const {appState} = useAppState({ 45 + onForeground: () => doPoll(true), 46 + }) 47 + const isScreenFocused = useIsFocused() 48 + const hasNew = feed.hasNewLatest && !feed.isRefreshing 49 + 50 + React.useEffect(() => { 51 + // called on first load 52 + if (!feed.hasLoaded && isPageFocused) { 53 + feed.setup() 54 + } 55 + }, [isPageFocused, feed]) 56 + 57 + const doPoll = React.useCallback( 58 + (knownActive = false) => { 59 + if ( 60 + (!knownActive && appState !== 'active') || 61 + !isScreenFocused || 62 + !isPageFocused 63 + ) { 64 + return 65 + } 66 + if (feed.isLoading) { 67 + return 68 + } 69 + store.log.debug('HomeScreen: Polling for new posts') 70 + feed.checkForLatest() 71 + }, 72 + [appState, isScreenFocused, isPageFocused, store, feed], 73 + ) 74 + 75 + const scrollToTop = React.useCallback(() => { 76 + scrollElRef.current?.scrollToOffset({offset: -headerOffset}) 77 + resetMainScroll() 78 + }, [headerOffset, resetMainScroll]) 79 + 80 + const onSoftReset = React.useCallback(() => { 81 + if (isPageFocused) { 82 + scrollToTop() 83 + feed.refresh() 84 + } 85 + }, [isPageFocused, scrollToTop, feed]) 86 + 87 + // fires when page within screen is activated/deactivated 88 + // - check for latest 89 + React.useEffect(() => { 90 + if (!isPageFocused || !isScreenFocused) { 91 + return 92 + } 93 + 94 + const softResetSub = store.onScreenSoftReset(onSoftReset) 95 + const feedCleanup = feed.registerListeners() 96 + const pollInterval = setInterval(doPoll, POLL_FREQ) 97 + 98 + screen('Feed') 99 + store.log.debug('HomeScreen: Updating feed') 100 + feed.checkForLatest() 101 + 102 + return () => { 103 + clearInterval(pollInterval) 104 + softResetSub.remove() 105 + feedCleanup() 106 + } 107 + }, [store, doPoll, onSoftReset, screen, feed, isPageFocused, isScreenFocused]) 108 + 109 + const onPressCompose = React.useCallback(() => { 110 + track('HomeScreen:PressCompose') 111 + store.shell.openComposer({}) 112 + }, [store, track]) 113 + 114 + const onPressTryAgain = React.useCallback(() => { 115 + feed.refresh() 116 + }, [feed]) 117 + 118 + const onPressLoadLatest = React.useCallback(() => { 119 + scrollToTop() 120 + feed.refresh() 121 + }, [feed, scrollToTop]) 122 + 123 + const ListHeaderComponent = React.useCallback(() => { 124 + if (isDesktop) { 125 + return ( 126 + <View 127 + style={[ 128 + pal.view, 129 + { 130 + flexDirection: 'row', 131 + alignItems: 'center', 132 + justifyContent: 'space-between', 133 + paddingHorizontal: 18, 134 + paddingVertical: 12, 135 + }, 136 + ]}> 137 + <TextLink 138 + type="title-lg" 139 + href="/" 140 + style={[pal.text, {fontWeight: 'bold'}]} 141 + text={ 142 + <> 143 + {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} 144 + {hasNew && ( 145 + <View 146 + style={{ 147 + top: -8, 148 + backgroundColor: colors.blue3, 149 + width: 8, 150 + height: 8, 151 + borderRadius: 4, 152 + }} 153 + /> 154 + )} 155 + </> 156 + } 157 + onPress={() => store.emitScreenSoftReset()} 158 + /> 159 + <TextLink 160 + type="title-lg" 161 + href="/settings/home-feed" 162 + style={{fontWeight: 'bold'}} 163 + accessibilityLabel="Feed Preferences" 164 + accessibilityHint="" 165 + text={ 166 + <FontAwesomeIcon 167 + icon="sliders" 168 + style={pal.textLight as FontAwesomeIconStyle} 169 + /> 170 + } 171 + /> 172 + </View> 173 + ) 174 + } 175 + return <></> 176 + }, [isDesktop, pal, store, hasNew]) 177 + 178 + return ( 179 + <View testID={testID} style={s.h100pct}> 180 + <Feed 181 + testID={testID ? `${testID}-feed` : undefined} 182 + key="default" 183 + feed={feed} 184 + scrollElRef={scrollElRef} 185 + onPressTryAgain={onPressTryAgain} 186 + onScroll={onMainScroll} 187 + scrollEventThrottle={100} 188 + renderEmptyState={renderEmptyState} 189 + renderEndOfFeed={renderEndOfFeed} 190 + ListHeaderComponent={ListHeaderComponent} 191 + headerOffset={headerOffset} 192 + /> 193 + {(isScrolledDown || hasNew) && ( 194 + <LoadLatestBtn 195 + onPress={onPressLoadLatest} 196 + label="Load new posts" 197 + showIndicator={hasNew} 198 + /> 199 + )} 200 + <FAB 201 + testID="composeFAB" 202 + onPress={onPressCompose} 203 + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 204 + accessibilityRole="button" 205 + accessibilityLabel="New post" 206 + accessibilityHint="" 207 + /> 208 + </View> 209 + ) 210 + })
+8 -20
src/view/com/pager/FeedsTabBar.web.tsx
··· 1 1 import React, {useMemo} from 'react' 2 - import {Animated, StyleSheet} from 'react-native' 2 + import {StyleSheet} from 'react-native' 3 + import Animated from 'react-native-reanimated' 3 4 import {observer} from 'mobx-react-lite' 4 5 import {TabBar} from 'view/com/pager/TabBar' 5 6 import {RenderTabBarFnProps} from 'view/com/pager/Pager' 6 7 import {useStores} from 'state/index' 7 8 import {usePalette} from 'lib/hooks/usePalette' 8 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 9 9 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 10 import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile' 11 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 11 12 12 13 export const FeedsTabBar = observer(function FeedsTabBarImpl( 13 14 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ··· 31 32 [store.me.savedFeeds.pinnedFeedNames], 32 33 ) 33 34 const pal = usePalette('default') 34 - const interp = useAnimatedValue(0) 35 - 36 - React.useEffect(() => { 37 - Animated.timing(interp, { 38 - toValue: store.shell.minimalShellMode ? 1 : 0, 39 - duration: 100, 40 - useNativeDriver: true, 41 - isInteraction: false, 42 - }).start() 43 - }, [interp, store.shell.minimalShellMode]) 44 - const transform = { 45 - transform: [ 46 - {translateX: '-50%'}, 47 - {translateY: Animated.multiply(interp, -100)}, 48 - ], 49 - } 35 + const {headerMinimalShellTransform} = useMinimalShellMode() 50 36 51 37 return ( 52 38 // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf 53 - <Animated.View style={[pal.view, styles.tabBar, transform]}> 39 + <Animated.View 40 + style={[pal.view, styles.tabBar, headerMinimalShellTransform]}> 54 41 <TabBar 55 42 key={items.join(',')} 56 43 {...props} ··· 65 52 tabBar: { 66 53 position: 'absolute', 67 54 zIndex: 1, 68 - left: '50%', 55 + // @ts-ignore Web only -prf 56 + left: 'calc(50% - 299px)', 69 57 width: 598, 70 58 top: 0, 71 59 flexDirection: 'row',
+14 -22
src/view/com/pager/FeedsTabBarMobile.tsx
··· 1 1 import React, {useMemo} from 'react' 2 - import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' 2 + import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 - import {autorun} from 'mobx' 5 4 import {TabBar} from 'view/com/pager/TabBar' 6 5 import {RenderTabBarFnProps} from 'view/com/pager/Pager' 7 6 import {useStores} from 'state/index' 8 7 import {usePalette} from 'lib/hooks/usePalette' 9 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 10 8 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 11 9 import {Link} from '../util/Link' 12 10 import {Text} from '../util/text/Text' ··· 14 12 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 15 13 import {s} from 'lib/styles' 16 14 import {HITSLOP_10} from 'lib/constants' 15 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 16 + import Animated from 'react-native-reanimated' 17 17 18 18 export const FeedsTabBar = observer(function FeedsTabBarImpl( 19 19 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 20 20 ) { 21 21 const store = useStores() 22 22 const pal = usePalette('default') 23 - const interp = useAnimatedValue(0) 24 - 25 - React.useEffect(() => { 26 - return autorun(() => { 27 - Animated.timing(interp, { 28 - toValue: store.shell.minimalShellMode ? 1 : 0, 29 - duration: 150, 30 - useNativeDriver: true, 31 - isInteraction: false, 32 - }).start() 33 - }) 34 - }, [interp, store]) 35 - const transform = { 36 - opacity: Animated.subtract(1, interp), 37 - transform: [{translateY: Animated.multiply(interp, -50)}], 38 - } 39 23 40 24 const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) 25 + const {headerMinimalShellTransform} = useMinimalShellMode() 41 26 42 27 const onPressAvi = React.useCallback(() => { 43 28 store.shell.openDrawer() ··· 48 33 [store.me.savedFeeds.pinnedFeedNames], 49 34 ) 50 35 36 + const tabBarKey = useMemo(() => { 37 + return items.join(',') 38 + }, [items]) 39 + 51 40 return ( 52 41 <Animated.View 53 42 style={[ 54 43 pal.view, 55 44 pal.border, 56 45 styles.tabBar, 57 - transform, 46 + headerMinimalShellTransform, 58 47 store.shell.minimalShellMode && styles.disabled, 59 48 ]}> 60 49 <View style={[pal.view, styles.topBar]}> ··· 92 81 </View> 93 82 </View> 94 83 <TabBar 95 - key={items.join(',')} 96 - {...props} 84 + key={tabBarKey} 85 + onPressSelected={props.onPressSelected} 86 + selectedPage={props.selectedPage} 87 + onSelect={props.onSelect} 88 + testID={props.testID} 97 89 items={items} 98 90 indicatorColor={pal.colors.link} 99 91 />
+1
src/view/com/pager/TabBar.tsx
··· 64 64 ) 65 65 66 66 const styles = isDesktop || isTablet ? desktopStyles : mobileStyles 67 + 67 68 return ( 68 69 <View testID={testID} style={[pal.view, styles.outer]}> 69 70 <DraggableScrollView
+2 -2
src/view/com/posts/Feed.tsx
··· 96 96 }, [feed, track, setIsRefreshing]) 97 97 98 98 const onEndReached = React.useCallback(async () => { 99 - if (!feed.hasLoaded) return 99 + if (!feed.hasLoaded || !feed.hasMore) return 100 100 101 101 track('Feed:onEndReached') 102 102 try { ··· 178 178 scrollEventThrottle={scrollEventThrottle} 179 179 indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 180 180 onEndReached={onEndReached} 181 - onEndReachedThreshold={0.6} 181 + onEndReachedThreshold={2} 182 182 removeClippedSubviews={true} 183 183 contentOffset={{x: 0, y: headerOffset * -1}} 184 184 extraData={extraData}
+2 -2
src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
··· 223 223 224 224 const onPress = React.useCallback(async () => { 225 225 try { 226 - const {following} = await toggle() 226 + const {following: isFollowing} = await toggle() 227 227 228 - if (following) { 228 + if (isFollowing) { 229 229 track('ProfileHeader:SuggestedFollowFollowed') 230 230 } 231 231 } catch (e: any) {
+5 -29
src/view/com/util/ViewHeader.tsx
··· 1 1 import React from 'react' 2 2 import {observer} from 'mobx-react-lite' 3 - import {autorun} from 'mobx' 4 - import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' 3 + import {StyleSheet, TouchableOpacity, View} from 'react-native' 5 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 5 import {useNavigation} from '@react-navigation/native' 7 6 import {CenteredView} from './Views' 8 7 import {Text} from './text/Text' 9 8 import {useStores} from 'state/index' 10 9 import {usePalette} from 'lib/hooks/usePalette' 11 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 12 10 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 13 11 import {useAnalytics} from 'lib/analytics/analytics' 14 12 import {NavigationProp} from 'lib/routes/types' 13 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 14 + import Animated from 'react-native-reanimated' 15 15 16 16 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 17 17 ··· 150 150 hideOnScroll: boolean 151 151 showBorder?: boolean 152 152 }) { 153 - const store = useStores() 154 153 const pal = usePalette('default') 155 - const interp = useAnimatedValue(0) 156 - 157 - React.useEffect(() => { 158 - return autorun(() => { 159 - if (store.shell.minimalShellMode) { 160 - Animated.timing(interp, { 161 - toValue: 1, 162 - duration: 100, 163 - useNativeDriver: true, 164 - isInteraction: false, 165 - }).start() 166 - } else { 167 - Animated.timing(interp, { 168 - toValue: 0, 169 - duration: 100, 170 - useNativeDriver: true, 171 - isInteraction: false, 172 - }).start() 173 - } 174 - }) 175 - }, [interp, store]) 176 - const transform = { 177 - transform: [{translateY: Animated.multiply(interp, -100)}], 178 - } 154 + const {headerMinimalShellTransform} = useMinimalShellMode() 179 155 180 156 if (!hideOnScroll) { 181 157 return ( ··· 198 174 styles.headerFloating, 199 175 pal.view, 200 176 pal.border, 201 - transform, 177 + headerMinimalShellTransform, 202 178 showBorder && styles.border, 203 179 ]}> 204 180 {children}
+25 -26
src/view/com/util/fab/FABInner.tsx
··· 1 1 import React, {ComponentProps} from 'react' 2 2 import {observer} from 'mobx-react-lite' 3 - import {autorun} from 'mobx' 4 - import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native' 3 + import {StyleSheet, TouchableWithoutFeedback} from 'react-native' 5 4 import LinearGradient from 'react-native-linear-gradient' 6 5 import {gradients} from 'lib/styles' 7 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 8 - import {useStores} from 'state/index' 9 6 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 7 import {useSafeAreaInsets} from 'react-native-safe-area-context' 11 8 import {clamp} from 'lib/numbers' 9 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 10 + import Animated from 'react-native-reanimated' 12 11 13 12 export interface FABProps 14 13 extends ComponentProps<typeof TouchableWithoutFeedback> { ··· 22 21 ...props 23 22 }: FABProps) { 24 23 const insets = useSafeAreaInsets() 25 - const {isTablet} = useWebMediaQueries() 26 - const store = useStores() 27 - const interp = useAnimatedValue(0) 28 - React.useEffect(() => { 29 - return autorun(() => { 30 - Animated.timing(interp, { 31 - toValue: store.shell.minimalShellMode ? 0 : 1, 32 - duration: 100, 33 - useNativeDriver: true, 34 - isInteraction: false, 35 - }).start() 36 - }) 37 - }, [interp, store]) 38 - const transform = isTablet 39 - ? undefined 40 - : { 41 - transform: [{translateY: Animated.multiply(interp, -44)}], 42 - } 43 - const size = isTablet ? styles.sizeLarge : styles.sizeRegular 44 - const right = isTablet ? 50 : 24 45 - const bottom = isTablet ? 50 : clamp(insets.bottom, 15, 60) + 15 24 + const {isMobile, isTablet} = useWebMediaQueries() 25 + const {fabMinimalShellTransform} = useMinimalShellMode() 26 + 27 + const size = React.useMemo(() => { 28 + return isTablet ? styles.sizeLarge : styles.sizeRegular 29 + }, [isTablet]) 30 + const tabletSpacing = React.useMemo(() => { 31 + return isTablet 32 + ? {right: 50, bottom: 50} 33 + : { 34 + right: 24, 35 + bottom: clamp(insets.bottom, 15, 60) + 15, 36 + } 37 + }, [insets.bottom, isTablet]) 38 + 46 39 return ( 47 40 <TouchableWithoutFeedback testID={testID} {...props}> 48 - <Animated.View style={[styles.outer, size, {right, bottom}, transform]}> 41 + <Animated.View 42 + style={[ 43 + styles.outer, 44 + size, 45 + tabletSpacing, 46 + isMobile && fabMinimalShellTransform, 47 + ]}> 49 48 <LinearGradient 50 49 colors={[gradients.blueLight.start, gradients.blueLight.end]} 51 50 start={{x: 0, y: 0}}
+8 -23
src/view/com/util/load-latest/LoadLatestBtn.tsx
··· 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 - import {useStores} from 'state/index' 7 5 import {usePalette} from 'lib/hooks/usePalette' 8 6 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 9 7 import {colors} from 'lib/styles' 10 8 import {HITSLOP_20} from 'lib/constants' 11 - import {isWeb} from 'platform/detection' 12 - import {clamp} from 'lib/numbers' 13 - import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated' 14 - 9 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 10 + import Animated from 'react-native-reanimated' 15 11 const AnimatedTouchableOpacity = 16 12 Animated.createAnimatedComponent(TouchableOpacity) 17 13 ··· 23 19 onPress: () => void 24 20 label: string 25 21 showIndicator: boolean 26 - minimalShellMode?: boolean // NOTE not used on mobile -prf 27 22 }) { 28 - const store = useStores() 29 23 const pal = usePalette('default') 30 - const {isDesktop, isTablet} = useWebMediaQueries() 31 - const safeAreaInsets = useSafeAreaInsets() 32 - const minMode = store.shell.minimalShellMode 33 - const bottom = isTablet 34 - ? 50 35 - : (minMode || isDesktop ? 16 : 60) + 36 - (isWeb ? 20 : clamp(safeAreaInsets.bottom, 15, 60)) 37 - const animatedStyle = useAnimatedStyle(() => ({ 38 - bottom: withTiming(bottom, {duration: 150}), 39 - })) 24 + const {isDesktop, isTablet, isMobile} = useWebMediaQueries() 25 + const {fabMinimalShellTransform} = useMinimalShellMode() 26 + 40 27 return ( 41 28 <AnimatedTouchableOpacity 42 29 style={[ ··· 45 32 isTablet && styles.loadLatestTablet, 46 33 pal.borderDark, 47 34 pal.view, 48 - animatedStyle, 35 + isMobile && fabMinimalShellTransform, 49 36 ]} 50 37 onPress={onPress} 51 38 hitSlop={HITSLOP_20} ··· 73 60 }, 74 61 loadLatestTablet: { 75 62 // @ts-ignore web only 76 - left: '50vw', 77 - transform: [{translateX: -282}], 63 + left: 'calc(50vw - 282px)', 78 64 }, 79 65 loadLatestDesktop: { 80 66 // @ts-ignore web only 81 - left: '50vw', 82 - transform: [{translateX: -382}], 67 + left: 'calc(50vw - 382px)', 83 68 }, 84 69 indicator: { 85 70 position: 'absolute',
+9 -211
src/view/screens/Home.tsx
··· 1 1 import React from 'react' 2 - import {FlatList, View, useWindowDimensions} from 'react-native' 3 - import {useFocusEffect, useIsFocused} from '@react-navigation/native' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 2 + import {useWindowDimensions} from 'react-native' 3 + import {useFocusEffect} from '@react-navigation/native' 6 4 import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' 7 5 import {observer} from 'mobx-react-lite' 8 - import useAppState from 'react-native-appstate-hook' 9 6 import isEqual from 'lodash.isequal' 10 7 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' 11 8 import {PostsFeedModel} from 'state/models/feeds/posts' 12 9 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 13 - import {TextLink} from 'view/com/util/Link' 14 - import {Feed} from '../com/posts/Feed' 15 10 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' 16 11 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' 17 12 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' 18 - import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' 19 13 import {FeedsTabBar} from '../com/pager/FeedsTabBar' 20 14 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' 21 - import {FAB} from '../com/util/fab/FAB' 22 15 import {useStores} from 'state/index' 23 - import {usePalette} from 'lib/hooks/usePalette' 24 - import {s, colors} from 'lib/styles' 25 - import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 26 - import {useAnalytics} from 'lib/analytics/analytics' 27 16 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 28 - import {ComposeIcon2} from 'lib/icons' 17 + import {FeedPage} from 'view/com/feeds/FeedPage' 29 18 30 - const POLL_FREQ = 30e3 // 30sec 19 + export const POLL_FREQ = 30e3 // 30sec 31 20 32 21 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> 33 22 export const HomeScreen = withAuthRequired( ··· 98 87 (props: RenderTabBarFnProps) => { 99 88 return ( 100 89 <FeedsTabBar 101 - {...props} 90 + key="FEEDS_TAB_BAR" 91 + selectedPage={props.selectedPage} 92 + onSelect={props.onSelect} 102 93 testID="homeScreenFeedTabs" 103 94 onPressSelected={onPressSelected} 104 95 /> ··· 109 100 110 101 const renderFollowingEmptyState = React.useCallback(() => { 111 102 return <FollowingEmptyState /> 112 - }, []) 113 - 114 - const renderFollowingEndOfFeed = React.useCallback(() => { 115 - return <FollowingEndOfFeed /> 116 103 }, []) 117 104 118 105 const renderCustomFeedEmptyState = React.useCallback(() => { ··· 132 119 isPageFocused={selectedPage === 0} 133 120 feed={store.me.mainFeed} 134 121 renderEmptyState={renderFollowingEmptyState} 135 - renderEndOfFeed={renderFollowingEndOfFeed} 122 + renderEndOfFeed={FollowingEndOfFeed} 136 123 /> 137 124 {customFeeds.map((f, index) => { 138 125 return ( ··· 150 137 }), 151 138 ) 152 139 153 - const FeedPage = observer(function FeedPageImpl({ 154 - testID, 155 - isPageFocused, 156 - feed, 157 - renderEmptyState, 158 - renderEndOfFeed, 159 - }: { 160 - testID?: string 161 - feed: PostsFeedModel 162 - isPageFocused: boolean 163 - renderEmptyState: () => JSX.Element 164 - renderEndOfFeed?: () => JSX.Element 165 - }) { 166 - const store = useStores() 167 - const pal = usePalette('default') 168 - const {isDesktop} = useWebMediaQueries() 169 - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll(store) 170 - const {screen, track} = useAnalytics() 171 - const headerOffset = useHeaderOffset() 172 - const scrollElRef = React.useRef<FlatList>(null) 173 - const {appState} = useAppState({ 174 - onForeground: () => doPoll(true), 175 - }) 176 - const isScreenFocused = useIsFocused() 177 - const hasNew = feed.hasNewLatest && !feed.isRefreshing 178 - 179 - React.useEffect(() => { 180 - // called on first load 181 - if (!feed.hasLoaded && isPageFocused) { 182 - feed.setup() 183 - } 184 - }, [isPageFocused, feed]) 185 - 186 - const doPoll = React.useCallback( 187 - (knownActive = false) => { 188 - if ( 189 - (!knownActive && appState !== 'active') || 190 - !isScreenFocused || 191 - !isPageFocused 192 - ) { 193 - return 194 - } 195 - if (feed.isLoading) { 196 - return 197 - } 198 - store.log.debug('HomeScreen: Polling for new posts') 199 - feed.checkForLatest() 200 - }, 201 - [appState, isScreenFocused, isPageFocused, store, feed], 202 - ) 203 - 204 - const scrollToTop = React.useCallback(() => { 205 - scrollElRef.current?.scrollToOffset({offset: -headerOffset}) 206 - resetMainScroll() 207 - }, [headerOffset, resetMainScroll]) 208 - 209 - const onSoftReset = React.useCallback(() => { 210 - if (isPageFocused) { 211 - scrollToTop() 212 - feed.refresh() 213 - } 214 - }, [isPageFocused, scrollToTop, feed]) 215 - 216 - // fires when page within screen is activated/deactivated 217 - // - check for latest 218 - React.useEffect(() => { 219 - if (!isPageFocused || !isScreenFocused) { 220 - return 221 - } 222 - 223 - const softResetSub = store.onScreenSoftReset(onSoftReset) 224 - const feedCleanup = feed.registerListeners() 225 - const pollInterval = setInterval(doPoll, POLL_FREQ) 226 - 227 - screen('Feed') 228 - store.log.debug('HomeScreen: Updating feed') 229 - feed.checkForLatest() 230 - 231 - return () => { 232 - clearInterval(pollInterval) 233 - softResetSub.remove() 234 - feedCleanup() 235 - } 236 - }, [store, doPoll, onSoftReset, screen, feed, isPageFocused, isScreenFocused]) 237 - 238 - const onPressCompose = React.useCallback(() => { 239 - track('HomeScreen:PressCompose') 240 - store.shell.openComposer({}) 241 - }, [store, track]) 242 - 243 - const onPressTryAgain = React.useCallback(() => { 244 - feed.refresh() 245 - }, [feed]) 246 - 247 - const onPressLoadLatest = React.useCallback(() => { 248 - scrollToTop() 249 - feed.refresh() 250 - }, [feed, scrollToTop]) 251 - 252 - const ListHeaderComponent = React.useCallback(() => { 253 - if (isDesktop) { 254 - return ( 255 - <View 256 - style={[ 257 - pal.view, 258 - { 259 - flexDirection: 'row', 260 - alignItems: 'center', 261 - justifyContent: 'space-between', 262 - paddingHorizontal: 18, 263 - paddingVertical: 12, 264 - }, 265 - ]}> 266 - <TextLink 267 - type="title-lg" 268 - href="/" 269 - style={[pal.text, {fontWeight: 'bold'}]} 270 - text={ 271 - <> 272 - {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} 273 - {hasNew && ( 274 - <View 275 - style={{ 276 - top: -8, 277 - backgroundColor: colors.blue3, 278 - width: 8, 279 - height: 8, 280 - borderRadius: 4, 281 - }} 282 - /> 283 - )} 284 - </> 285 - } 286 - onPress={() => store.emitScreenSoftReset()} 287 - /> 288 - <TextLink 289 - type="title-lg" 290 - href="/settings/home-feed" 291 - style={{fontWeight: 'bold'}} 292 - accessibilityLabel="Feed Preferences" 293 - accessibilityHint="" 294 - text={ 295 - <FontAwesomeIcon 296 - icon="sliders" 297 - style={pal.textLight as FontAwesomeIconStyle} 298 - /> 299 - } 300 - /> 301 - </View> 302 - ) 303 - } 304 - return <></> 305 - }, [isDesktop, pal, store, hasNew]) 306 - 307 - return ( 308 - <View testID={testID} style={s.h100pct}> 309 - <Feed 310 - testID={testID ? `${testID}-feed` : undefined} 311 - key="default" 312 - feed={feed} 313 - scrollElRef={scrollElRef} 314 - onPressTryAgain={onPressTryAgain} 315 - onScroll={onMainScroll} 316 - scrollEventThrottle={100} 317 - renderEmptyState={renderEmptyState} 318 - renderEndOfFeed={renderEndOfFeed} 319 - ListHeaderComponent={ListHeaderComponent} 320 - headerOffset={headerOffset} 321 - /> 322 - {(isScrolledDown || hasNew) && ( 323 - <LoadLatestBtn 324 - onPress={onPressLoadLatest} 325 - label="Load new posts" 326 - showIndicator={hasNew} 327 - minimalShellMode={store.shell.minimalShellMode} 328 - /> 329 - )} 330 - <FAB 331 - testID="composeFAB" 332 - onPress={onPressCompose} 333 - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 334 - accessibilityRole="button" 335 - accessibilityLabel="New post" 336 - accessibilityHint="" 337 - /> 338 - </View> 339 - ) 340 - }) 341 - 342 - function useHeaderOffset() { 140 + export function useHeaderOffset() { 343 141 const {isDesktop, isTablet} = useWebMediaQueries() 344 142 const {fontScale} = useWindowDimensions() 345 143 if (isDesktop) {
-1
src/view/screens/Notifications.tsx
··· 156 156 onPress={onPressLoadLatest} 157 157 label="Load new notifications" 158 158 showIndicator={hasNew} 159 - minimalShellMode={true} 160 159 /> 161 160 )} 162 161 </View>
+2 -6
src/view/shell/bottom-bar/BottomBar.tsx
··· 1 1 import React, {ComponentProps} from 'react' 2 - import { 3 - Animated, 4 - GestureResponderEvent, 5 - TouchableOpacity, 6 - View, 7 - } from 'react-native' 2 + import {GestureResponderEvent, TouchableOpacity, View} from 'react-native' 3 + import Animated from 'react-native-reanimated' 8 4 import {StackActions} from '@react-navigation/native' 9 5 import {BottomTabBarProps} from '@react-navigation/bottom-tabs' 10 6 import {useSafeAreaInsets} from 'react-native-safe-area-context'
+1 -1
src/view/shell/bottom-bar/BottomBarWeb.tsx
··· 2 2 import {observer} from 'mobx-react-lite' 3 3 import {useStores} from 'state/index' 4 4 import {usePalette} from 'lib/hooks/usePalette' 5 - import {Animated} from 'react-native' 6 5 import {useNavigationState} from '@react-navigation/native' 6 + import Animated from 'react-native-reanimated' 7 7 import {useSafeAreaInsets} from 'react-native-safe-area-context' 8 8 import {getCurrentRoute, isTab} from 'lib/routes/helpers' 9 9 import {styles} from './BottomBarStyles'
+116 -2
yarn.lock
··· 3744 3744 "@sentry/utils" "7.52.1" 3745 3745 tslib "^1.9.3" 3746 3746 3747 + "@sentry-internal/tracing@7.69.0": 3748 + version "7.69.0" 3749 + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.69.0.tgz#8d8eb740b72967b6ba3fdc0a5173aa55331b7d35" 3750 + integrity sha512-4BgeWZUj9MO6IgfO93C9ocP3+AdngqujF/+zB2rFdUe+y9S6koDyUC7jr9Knds/0Ta72N/0D6PwhgSCpHK8s0Q== 3751 + dependencies: 3752 + "@sentry/core" "7.69.0" 3753 + "@sentry/types" "7.69.0" 3754 + "@sentry/utils" "7.69.0" 3755 + tslib "^2.4.1 || ^1.9.3" 3756 + 3747 3757 "@sentry/browser@7.52.0": 3748 3758 version "7.52.0" 3749 3759 resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.52.0.tgz#55d266c89ed668389ff687e5cc885c27016ea85c" ··· 3768 3778 "@sentry/utils" "7.52.1" 3769 3779 tslib "^1.9.3" 3770 3780 3781 + "@sentry/browser@7.69.0": 3782 + version "7.69.0" 3783 + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.69.0.tgz#65427c90fb71c1775e2c1e38431efb7f4aec1e34" 3784 + integrity sha512-5ls+zu2PrMhHCIIhclKQsWX5u6WH0Ez5/GgrCMZTtZ1d70ukGSRUvpZG9qGf5Cw1ezS1LY+1HCc3whf8x8lyPw== 3785 + dependencies: 3786 + "@sentry-internal/tracing" "7.69.0" 3787 + "@sentry/core" "7.69.0" 3788 + "@sentry/replay" "7.69.0" 3789 + "@sentry/types" "7.69.0" 3790 + "@sentry/utils" "7.69.0" 3791 + tslib "^2.4.1 || ^1.9.3" 3792 + 3771 3793 "@sentry/cli@2.17.5": 3772 3794 version "2.17.5" 3773 3795 resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.17.5.tgz#d41e24893a843bcd41e14274044a7ddea9332824" ··· 3779 3801 proxy-from-env "^1.1.0" 3780 3802 which "^2.0.2" 3781 3803 3804 + "@sentry/cli@2.20.7": 3805 + version "2.20.7" 3806 + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.20.7.tgz#8f7f3f632c330cac6bd2278d820948163f3128a6" 3807 + integrity sha512-YaHKEUdsFt59nD8yLvuEGCOZ3/ArirL8GZ/66RkZ8wcD2wbpzOFbzo08Kz4te/Eo3OD5/RdW+1dPaOBgGbrXlA== 3808 + dependencies: 3809 + https-proxy-agent "^5.0.0" 3810 + node-fetch "^2.6.7" 3811 + progress "^2.0.3" 3812 + proxy-from-env "^1.1.0" 3813 + which "^2.0.2" 3814 + 3782 3815 "@sentry/core@7.52.0": 3783 3816 version "7.52.0" 3784 3817 resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.52.0.tgz#6c820ca48fe2f06bfd6b290044c96de2375f2ad4" ··· 3796 3829 "@sentry/types" "7.52.1" 3797 3830 "@sentry/utils" "7.52.1" 3798 3831 tslib "^1.9.3" 3832 + 3833 + "@sentry/core@7.69.0": 3834 + version "7.69.0" 3835 + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.69.0.tgz#ebbe01df573f438f8613107020a4e18eb9adca4d" 3836 + integrity sha512-V6jvK2lS8bhqZDMFUtvwe2XvNstFQf5A+2LMKCNBOV/NN6eSAAd6THwEpginabjet9dHsNRmMk7WNKvrUfQhZw== 3837 + dependencies: 3838 + "@sentry/types" "7.69.0" 3839 + "@sentry/utils" "7.69.0" 3840 + tslib "^2.4.1 || ^1.9.3" 3799 3841 3800 3842 "@sentry/hub@7.52.0": 3801 3843 version "7.52.0" ··· 3807 3849 "@sentry/utils" "7.52.0" 3808 3850 tslib "^1.9.3" 3809 3851 3852 + "@sentry/hub@7.69.0": 3853 + version "7.69.0" 3854 + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.69.0.tgz#3ef3b98e1810b05cb4fb37a861bd700ef592a2a9" 3855 + integrity sha512-71TQ7P5de9+cdW1ETGI9wgi2VNqfyWaM3cnUvheXaSjPRBrr6mhwoaSjo+GGsiwx97Ob9DESZEIhdzcLupzkFA== 3856 + dependencies: 3857 + "@sentry/core" "7.69.0" 3858 + "@sentry/types" "7.69.0" 3859 + "@sentry/utils" "7.69.0" 3860 + tslib "^2.4.1 || ^1.9.3" 3861 + 3810 3862 "@sentry/integrations@7.52.0": 3811 3863 version "7.52.0" 3812 3864 resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.52.0.tgz#632aa5e54bdfdab910a24057c2072634a2670409" ··· 3827 3879 localforage "^1.8.1" 3828 3880 tslib "^1.9.3" 3829 3881 3882 + "@sentry/integrations@7.69.0": 3883 + version "7.69.0" 3884 + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.69.0.tgz#04c0206d9436ec7b79971e3bde5d6e1e9194595f" 3885 + integrity sha512-FEFtFqXuCo9+L7bENZxFpEAlIODwHl6FyW/DwLfniy9jOXHU7BhP/oICLrFE5J7rh1gNY7N/8VlaiQr3hCnS/g== 3886 + dependencies: 3887 + "@sentry/types" "7.69.0" 3888 + "@sentry/utils" "7.69.0" 3889 + localforage "^1.8.1" 3890 + tslib "^2.4.1 || ^1.9.3" 3891 + 3892 + "@sentry/react-native@5.10.0": 3893 + version "5.10.0" 3894 + resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-5.10.0.tgz#b61861276fcb35e69dbe9c4e098ed7c88598f5d9" 3895 + integrity sha512-YuEZJ3tW5qZlFGFm2FoAZ9vw1fWnjrhMh1IHxo+nUHP3FvVgGkAd/PmSSbgPr2T3YLOIJNiyDdG031Qi7YvtGA== 3896 + dependencies: 3897 + "@sentry/browser" "7.69.0" 3898 + "@sentry/cli" "2.20.7" 3899 + "@sentry/core" "7.69.0" 3900 + "@sentry/hub" "7.69.0" 3901 + "@sentry/integrations" "7.69.0" 3902 + "@sentry/react" "7.69.0" 3903 + "@sentry/types" "7.69.0" 3904 + "@sentry/utils" "7.69.0" 3905 + 3830 3906 "@sentry/react-native@5.5.0": 3831 3907 version "5.5.0" 3832 3908 resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-5.5.0.tgz#b1283f68465b1772ad6059ebba149673cef33f2d" ··· 3863 3939 hoist-non-react-statics "^3.3.2" 3864 3940 tslib "^1.9.3" 3865 3941 3942 + "@sentry/react@7.69.0": 3943 + version "7.69.0" 3944 + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.69.0.tgz#b9931ac590d8dad3390a9a03a516f1b1bd75615e" 3945 + integrity sha512-J+DciRRVuruf1nMmBOi2VeJkOLGeCb4vTOFmHzWTvRJNByZ0flyo8E/fyROL7+23kBq1YbcVY6IloUlH73hneQ== 3946 + dependencies: 3947 + "@sentry/browser" "7.69.0" 3948 + "@sentry/types" "7.69.0" 3949 + "@sentry/utils" "7.69.0" 3950 + hoist-non-react-statics "^3.3.2" 3951 + tslib "^2.4.1 || ^1.9.3" 3952 + 3866 3953 "@sentry/replay@7.52.0": 3867 3954 version "7.52.0" 3868 3955 resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.52.0.tgz#4d78e88282d2c1044ea4b648a68d1b22173e810d" ··· 3880 3967 "@sentry/core" "7.52.1" 3881 3968 "@sentry/types" "7.52.1" 3882 3969 "@sentry/utils" "7.52.1" 3970 + 3971 + "@sentry/replay@7.69.0": 3972 + version "7.69.0" 3973 + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.69.0.tgz#d727f96292d2b7c25df022fa53764fd39910fcda" 3974 + integrity sha512-oUqWyBPFUgShdVvgJtV65EQH9pVDmoYVQMOu59JI6FHVeL3ald7R5Mvz6GaNLXsirvvhp0yAkcAd2hc5Xi6hDw== 3975 + dependencies: 3976 + "@sentry/core" "7.69.0" 3977 + "@sentry/types" "7.69.0" 3978 + "@sentry/utils" "7.69.0" 3883 3979 3884 3980 "@sentry/types@7.52.0": 3885 3981 version "7.52.0" ··· 3891 3987 resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.52.1.tgz#bcff6d0462d9b9b7b9ec31c0068fe02d44f25da2" 3892 3988 integrity sha512-OMbGBPrJsw0iEXwZ2bJUYxewI1IEAU2e1aQGc0O6QW5+6hhCh+8HO8Xl4EymqwejjztuwStkl6G1qhK+Q0/Row== 3893 3989 3990 + "@sentry/types@7.69.0": 3991 + version "7.69.0" 3992 + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.69.0.tgz#012b8d90d270a473cc2a5cf58a56870542739292" 3993 + integrity sha512-zPyCox0mzitzU6SIa1KIbNoJAInYDdUpdiA+PoUmMn2hFMH1llGU/cS7f4w/mAsssTlbtlBi72RMnWUCy578bw== 3994 + 3894 3995 "@sentry/utils@7.52.0": 3895 3996 version "7.52.0" 3896 3997 resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.52.0.tgz#cacc36d905036ba7084c14965e964fc44239d7f0" ··· 3906 4007 dependencies: 3907 4008 "@sentry/types" "7.52.1" 3908 4009 tslib "^1.9.3" 4010 + 4011 + "@sentry/utils@7.69.0": 4012 + version "7.69.0" 4013 + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.69.0.tgz#b7594e4eb2a88b9b25298770b841dd3f81bd2aa4" 4014 + integrity sha512-4eBixe5Y+0EGVU95R4NxH3jkkjtkE4/CmSZD4In8SCkWGSauogePtq6hyiLsZuP1QHdpPb9Kt0+zYiBb2LouBA== 4015 + dependencies: 4016 + "@sentry/types" "7.69.0" 4017 + tslib "^2.4.1 || ^1.9.3" 3909 4018 3910 4019 "@sideway/address@^4.1.3": 3911 4020 version "4.1.4" ··· 6531 6640 version "0.4.24" 6532 6641 resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" 6533 6642 integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== 6643 + 6644 + babel-plugin-transform-remove-console@^6.9.4: 6645 + version "6.9.4" 6646 + resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780" 6647 + integrity sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg== 6534 6648 6535 6649 babel-preset-current-node-syntax@^1.0.0: 6536 6650 version "1.0.1" ··· 16704 16818 range-parser "~1.2.1" 16705 16819 statuses "2.0.1" 16706 16820 16707 - sentry-expo@~7.0.0: 16821 + sentry-expo@~7.0.1: 16708 16822 version "7.0.1" 16709 16823 resolved "https://registry.yarnpkg.com/sentry-expo/-/sentry-expo-7.0.1.tgz#025f0e90ab7f7cba1e00c892fabc027de21bc5bc" 16710 16824 integrity sha512-8vmOy4R+qM1peQA9EP8rDGUMBhgMU1D5FyuWY9kfNGatmWuvEmlZpVgaXoXaNPIhPgf2TMrvQIlbqLHtTkoeSA== ··· 17890 18004 resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 17891 18005 integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 17892 18006 17893 - tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1: 18007 + 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": 17894 18008 version "2.6.2" 17895 18009 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" 17896 18010 integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==