diff --git a/.gitignore b/.gitignore index 66658f8e..0bd7d137 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 00000000..2d32793e --- /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 00000000..e735c48c --- /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 598e2a56..706fdff5 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 00000000..8af163a8 --- /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 69e5c94d..402abecc 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 2629be2c..5d6bd4f1 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 eddf1dc4..c058c5ce 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": { @@ -53,7 +59,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", @@ -71,6 +77,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", @@ -148,7 +155,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 5ff4ddab..2962aa44 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/src/lib/hooks/useMinimalShellMode.tsx b/src/lib/hooks/useMinimalShellMode.tsx index 68f405dc..475d165d 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 00000000..725106d5 --- /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/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 02aa623c..dc91bd29 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -1,13 +1,14 @@ import React, {useMemo} from 'react' -import {Animated, StyleSheet} from 'react-native' +import {StyleSheet} from 'react-native' +import Animated from 'react-native-reanimated' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => 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 319d28f9..8614bdf6 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/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index 41e4022d..c5b187fb 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 3a34777a..ec459b4e 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 97eeba35..5b1d5d88 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 ( - + 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 97740135..b00bfb76 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 984aef25..cfd4d46d 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 e2021423..ebcc527a 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 bd7dbeaa..819488e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3744,6 +3744,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" @@ -3768,6 +3778,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" @@ -3779,6 +3801,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" @@ -3797,6 +3830,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" @@ -3807,6 +3849,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" @@ -3827,6 +3879,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" @@ -3863,6 +3939,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" @@ -3881,6 +3968,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" @@ -3891,6 +3987,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" @@ -3907,6 +4008,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" @@ -6532,6 +6641,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" @@ -16704,7 +16818,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== @@ -17890,7 +18004,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==