diff options
author | Ansh <anshnanda10@gmail.com> | 2023-10-13 18:54:35 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-13 18:54:35 -0700 |
commit | 8e9cf182c2e247203b6b5ea9ae701c039945d6a0 (patch) | |
tree | 1dbf0c69fe209fccdb7841b29fc03bf8e311eac3 /src | |
parent | 9042f503c2533deff535de75b190c26ed1ae59ec (diff) | |
download | voidsky-8e9cf182c2e247203b6b5ea9ae701c039945d6a0.tar.zst |
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>
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/hooks/useMinimalShellMode.tsx | 66 | ||||
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 210 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 28 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 36 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 1 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 4 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeaderSuggestedFollows.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/ViewHeader.tsx | 34 | ||||
-rw-r--r-- | src/view/com/util/fab/FABInner.tsx | 51 | ||||
-rw-r--r-- | src/view/com/util/load-latest/LoadLatestBtn.tsx | 31 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 220 | ||||
-rw-r--r-- | src/view/screens/Notifications.tsx | 1 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 8 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBarWeb.tsx | 2 |
14 files changed, 332 insertions, 364 deletions
diff --git a/src/lib/hooks/useMinimalShellMode.tsx b/src/lib/hooks/useMinimalShellMode.tsx index 68f405dc4..475d165d3 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 000000000..725106d59 --- /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<FlatList>(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 ( + <View + style={[ + pal.view, + { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 18, + paddingVertical: 12, + }, + ]}> + <TextLink + type="title-lg" + href="/" + style={[pal.text, {fontWeight: 'bold'}]} + text={ + <> + {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} + {hasNew && ( + <View + style={{ + top: -8, + backgroundColor: colors.blue3, + width: 8, + height: 8, + borderRadius: 4, + }} + /> + )} + </> + } + onPress={() => store.emitScreenSoftReset()} + /> + <TextLink + type="title-lg" + href="/settings/home-feed" + style={{fontWeight: 'bold'}} + accessibilityLabel="Feed Preferences" + accessibilityHint="" + text={ + <FontAwesomeIcon + icon="sliders" + style={pal.textLight as FontAwesomeIconStyle} + /> + } + /> + </View> + ) + } + return <></> + }, [isDesktop, pal, store, hasNew]) + + return ( + <View testID={testID} style={s.h100pct}> + <Feed + testID={testID ? `${testID}-feed` : undefined} + key="default" + feed={feed} + scrollElRef={scrollElRef} + onPressTryAgain={onPressTryAgain} + onScroll={onMainScroll} + scrollEventThrottle={100} + renderEmptyState={renderEmptyState} + renderEndOfFeed={renderEndOfFeed} + ListHeaderComponent={ListHeaderComponent} + headerOffset={headerOffset} + /> + {(isScrolledDown || hasNew) && ( + <LoadLatestBtn + onPress={onPressLoadLatest} + label="Load new posts" + showIndicator={hasNew} + /> + )} + <FAB + testID="composeFAB" + onPress={onPressCompose} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + accessibilityRole="button" + accessibilityLabel="New post" + accessibilityHint="" + /> + </View> + ) +}) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 02aa623cc..dc91bd296 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 - <Animated.View style={[pal.view, styles.tabBar, transform]}> + <Animated.View + style={[pal.view, styles.tabBar, headerMinimalShellTransform]}> <TabBar key={items.join(',')} {...props} @@ -65,7 +52,8 @@ const styles = StyleSheet.create({ tabBar: { position: 'absolute', zIndex: 1, - left: '50%', + // @ts-ignore Web only -prf + left: 'calc(50% - 299px)', width: 598, top: 0, flexDirection: 'row', diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 7924666e5..d8579badc 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -1,12 +1,10 @@ import React, {useMemo} from 'react' -import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {autorun} from 'mobx' 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 {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {Link} from '../util/Link' import {Text} from '../util/text/Text' @@ -14,30 +12,17 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {s} from 'lib/styles' import {HITSLOP_10} from 'lib/constants' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => 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 ( <Animated.View style={[ pal.view, pal.border, styles.tabBar, - transform, + headerMinimalShellTransform, store.shell.minimalShellMode && styles.disabled, ]}> <View style={[pal.view, styles.topBar]}> @@ -92,8 +81,11 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( </View> </View> <TabBar - key={items.join(',')} - {...props} + key={tabBarKey} + onPressSelected={props.onPressSelected} + selectedPage={props.selectedPage} + onSelect={props.onSelect} + testID={props.testID} items={items} indicatorColor={pal.colors.link} /> diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 319d28f95..8614bdf64 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 ( <View testID={testID} style={[pal.view, styles.outer]}> <DraggableScrollView diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 0de769aab..74883f82a 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -96,7 +96,7 @@ export const Feed = observer(function Feed({ }, [feed, track, setIsRefreshing]) const onEndReached = React.useCallback(async () => { - 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 41e4022d5..c5b187fb3 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 3a34777ab..ec459b4eb 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 97eeba358..5b1d5d888 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<typeof TouchableWithoutFeedback> { @@ -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 ( <TouchableWithoutFeedback testID={testID} {...props}> - <Animated.View style={[styles.outer, size, {right, bottom}, transform]}> + <Animated.View + style={[ + styles.outer, + size, + tabletSpacing, + isMobile && fabMinimalShellTransform, + ]}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index 57c3baa5b..b16a42396 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 ( <AnimatedTouchableOpacity style={[ @@ -45,7 +32,7 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ isTablet && styles.loadLatestTablet, pal.borderDark, pal.view, - animatedStyle, + isMobile && fabMinimalShellTransform, ]} onPress={onPress} hitSlop={HITSLOP_20} @@ -73,13 +60,11 @@ const styles = StyleSheet.create({ }, loadLatestTablet: { // @ts-ignore web only - left: '50vw', - transform: [{translateX: -282}], + left: 'calc(50vw - 282px)', }, loadLatestDesktop: { // @ts-ignore web only - left: '50vw', - transform: [{translateX: -382}], + left: 'calc(50vw - 382px)', }, indicator: { position: 'absolute', diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index cce62f498..ad47e9f9b 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,33 +1,22 @@ import React from 'react' -import {FlatList, View, useWindowDimensions} from 'react-native' -import {useFocusEffect, useIsFocused} from '@react-navigation/native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' +import {useWindowDimensions} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' import {observer} from 'mobx-react-lite' -import useAppState from 'react-native-appstate-hook' import isEqual from 'lodash.isequal' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {PostsFeedModel} from 'state/models/feeds/posts' import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {TextLink} from 'view/com/util/Link' -import {Feed} from '../com/posts/Feed' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' -import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' import {FeedsTabBar} from '../com/pager/FeedsTabBar' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' -import {FAB} from '../com/util/fab/FAB' import {useStores} from 'state/index' -import {usePalette} from 'lib/hooks/usePalette' -import {s, colors} from 'lib/styles' -import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' -import {useAnalytics} from 'lib/analytics/analytics' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {ComposeIcon2} from 'lib/icons' +import {FeedPage} from 'view/com/feeds/FeedPage' -const POLL_FREQ = 30e3 // 30sec +export const POLL_FREQ = 30e3 // 30sec type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> export const HomeScreen = withAuthRequired( @@ -98,7 +87,9 @@ export const HomeScreen = withAuthRequired( (props: RenderTabBarFnProps) => { return ( <FeedsTabBar - {...props} + key="FEEDS_TAB_BAR" + selectedPage={props.selectedPage} + onSelect={props.onSelect} testID="homeScreenFeedTabs" onPressSelected={onPressSelected} /> @@ -111,10 +102,6 @@ export const HomeScreen = withAuthRequired( return <FollowingEmptyState /> }, []) - const renderFollowingEndOfFeed = React.useCallback(() => { - return <FollowingEndOfFeed /> - }, []) - const renderCustomFeedEmptyState = React.useCallback(() => { return <CustomFeedEmptyState /> }, []) @@ -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<FlatList>(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 ( - <View - style={[ - pal.view, - { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 18, - paddingVertical: 12, - }, - ]}> - <TextLink - type="title-lg" - href="/" - style={[pal.text, {fontWeight: 'bold'}]} - text={ - <> - {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} - {hasNew && ( - <View - style={{ - top: -8, - backgroundColor: colors.blue3, - width: 8, - height: 8, - borderRadius: 4, - }} - /> - )} - </> - } - onPress={() => store.emitScreenSoftReset()} - /> - <TextLink - type="title-lg" - href="/settings/home-feed" - style={{fontWeight: 'bold'}} - accessibilityLabel="Feed Preferences" - accessibilityHint="" - text={ - <FontAwesomeIcon - icon="sliders" - style={pal.textLight as FontAwesomeIconStyle} - /> - } - /> - </View> - ) - } - return <></> - }, [isDesktop, pal, store, hasNew]) - - return ( - <View testID={testID} style={s.h100pct}> - <Feed - testID={testID ? `${testID}-feed` : undefined} - key="default" - feed={feed} - scrollElRef={scrollElRef} - onPressTryAgain={onPressTryAgain} - onScroll={onMainScroll} - scrollEventThrottle={100} - renderEmptyState={renderEmptyState} - renderEndOfFeed={renderEndOfFeed} - ListHeaderComponent={ListHeaderComponent} - headerOffset={headerOffset} - /> - {(isScrolledDown || hasNew) && ( - <LoadLatestBtn - onPress={onPressLoadLatest} - label="Load new posts" - showIndicator={hasNew} - minimalShellMode={store.shell.minimalShellMode} - /> - )} - <FAB - testID="composeFAB" - onPress={onPressCompose} - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} - accessibilityRole="button" - accessibilityLabel="New post" - accessibilityHint="" - /> - </View> - ) -}) - -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 977401350..b00bfb765 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} /> )} </View> diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 984aef25d..cfd4d46d0 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 e20214235..ebcc527a1 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' |