diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 2aacdb89..e6b5d1fb 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -1,30 +1,24 @@ import React from 'react' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import {useAnalytics} from 'lib/analytics/analytics' import {useQueryClient} from '@tanstack/react-query' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {MainScrollProvider} from '../util/MainScrollProvider' -import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useSetMinimalShellMode} from '#/state/shell' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {ComposeIcon2} from 'lib/icons' -import {colors, s} from 'lib/styles' +import {s} from 'lib/styles' import {View, useWindowDimensions} from 'react-native' import {ListMethods} from '../util/List' import {Feed} from '../posts/Feed' -import {TextLink} from '../util/Link' import {FAB} from '../util/fab/FAB' import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' -import {listenSoftReset, emitSoftReset} from '#/state/events' +import {listenSoftReset} from '#/state/events' import {truncateAndInvalidate} from '#/state/queries/util' import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers' import {isNative} from '#/platform/detection' @@ -47,10 +41,8 @@ export function FeedPage({ renderEndOfFeed?: () => JSX.Element }) { const {hasSession} = useSession() - const pal = usePalette('default') const {_} = useLingui() const navigation = useNavigation() - const {isDesktop} = useWebMediaQueries() const queryClient = useQueryClient() const {openComposer} = useComposerControls() const [isScrolledDown, setIsScrolledDown] = React.useState(false) @@ -99,63 +91,6 @@ export function FeedPage({ setHasNew(false) }, [scrollToTop, feed, queryClient, setHasNew]) - const ListHeaderComponent = React.useCallback(() => { - if (isDesktop) { - return ( - - - Bluesky{' '} - {hasNew && ( - - )} - - } - onPress={emitSoftReset} - /> - {hasSession && ( - - } - /> - )} - - ) - } - return <> - }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, hasSession]) - return ( @@ -171,7 +106,6 @@ export function FeedPage({ onHasNew={setHasNew} renderEmptyState={renderEmptyState} renderEndOfFeed={renderEndOfFeed} - ListHeaderComponent={ListHeaderComponent} headerOffset={headerOffset} /> diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx index 5ffa31f3..3df3858b 100644 --- a/src/view/com/home/HomeHeader.tsx +++ b/src/view/com/home/HomeHeader.tsx @@ -1,7 +1,6 @@ import React from 'react' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {HomeHeaderLayout} from './HomeHeaderLayout' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {usePinnedFeedsInfos} from '#/state/queries/feed' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' @@ -11,16 +10,6 @@ import {usePalette} from '#/lib/hooks/usePalette' export function HomeHeader( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, -) { - const {isDesktop} = useWebMediaQueries() - if (isDesktop) { - return null - } - return -} - -export function HomeHeaderInner( - props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const navigation = useNavigation() const {feeds, hasPinnedCustom} = usePinnedFeedsInfos() diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index 47cb0023..fbb55e6b 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -1,11 +1,20 @@ import React from 'react' -import {StyleSheet} from 'react-native' +import {StyleSheet, View} from 'react-native' import Animated from 'react-native-reanimated' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useShellLayout} from '#/state/shell/shell-layout' +import {Logo} from '#/view/icons/Logo' +import {Link, TextLink} from '../util/Link' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {CogIcon} from '#/lib/icons' export function HomeHeaderLayout({children}: {children: React.ReactNode}) { const {isMobile} = useWebMediaQueries() @@ -20,6 +29,7 @@ function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) { const pal = usePalette('default') const {headerMinimalShellTransform} = useMinimalShellMode() const {headerHeight} = useShellLayout() + const {_} = useLingui() return ( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf @@ -28,12 +38,44 @@ function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) { onLayout={e => { headerHeight.value = e.nativeEvent.layout.height }}> + + + } + /> + + + + + {children} ) } const styles = StyleSheet.create({ + topBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 18, + paddingVertical: 8, + marginTop: 8, + width: '100%', + }, tabBar: { // @ts-ignore Web only position: 'sticky', @@ -42,7 +84,7 @@ const styles = StyleSheet.create({ left: 'calc(50% - 300px)', width: 600, top: 0, - flexDirection: 'row', + flexDirection: 'column', alignItems: 'center', borderLeftWidth: 1, borderRightWidth: 1, diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 6c4b911f..f51efb7b 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -103,7 +103,6 @@ const styles = StyleSheet.create({ right: 0, top: 0, flexDirection: 'column', - borderBottomWidth: 1, }, topBar: { flexDirection: 'row', diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 3204bb23..ff8acd60 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -5,6 +5,7 @@ import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {DraggableScrollView} from './DraggableScrollView' +import {isNative} from '#/platform/detection' export interface TabBarProps { testID?: string @@ -15,6 +16,10 @@ export interface TabBarProps { onPressSelected?: (index: number) => void } +// How much of the previous/next item we're showing +// to give the user a hint there's more to scroll. +const OFFSCREEN_ITEM_WIDTH = 20 + export function TabBar({ testID, selectedPage, @@ -25,6 +30,7 @@ export function TabBar({ }: TabBarProps) { const pal = usePalette('default') const scrollElRef = useRef(null) + const itemRefs = useRef>([]) const [itemXs, setItemXs] = useState([]) const indicatorStyle = useMemo( () => ({borderBottomColor: indicatorColor || pal.colors.link}), @@ -33,12 +39,58 @@ export function TabBar({ const {isDesktop, isTablet} = useWebMediaQueries() const styles = isDesktop || isTablet ? desktopStyles : mobileStyles - // scrolls to the selected item when the page changes useEffect(() => { - scrollElRef.current?.scrollTo({ - x: - (itemXs[selectedPage] || 0) - styles.contentContainer.paddingHorizontal, - }) + if (isNative) { + // On native, the primary interaction is swiping. + // We adjust the scroll little by little on every tab change. + // Scroll into view but keep the end of the previous item visible. + let x = itemXs[selectedPage] || 0 + x = Math.max(0, x - OFFSCREEN_ITEM_WIDTH) + scrollElRef.current?.scrollTo({x}) + } else { + // On the web, the primary interaction is tapping. + // Scrolling under tap feels disorienting so only adjust the scroll offset + // when tapping on an item out of view--and we adjust by almost an entire page. + const parent = scrollElRef?.current?.getScrollableNode?.() + if (!parent) { + return + } + const parentRect = parent.getBoundingClientRect() + if (!parentRect) { + return + } + const { + left: parentLeft, + right: parentRight, + width: parentWidth, + } = parentRect + const child = itemRefs.current[selectedPage] + if (!child) { + return + } + const childRect = child.getBoundingClientRect?.() + if (!childRect) { + return + } + const {left: childLeft, right: childRight, width: childWidth} = childRect + let dx = 0 + if (childRight >= parentRight) { + dx += childRight - parentRight + dx += parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH + } else if (childLeft <= parentLeft) { + dx -= parentLeft - childLeft + dx -= parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH + } + let x = parent.scrollLeft + dx + x = Math.max(0, x) + x = Math.min(x, parent.scrollWidth - parentWidth) + if (dx !== 0) { + parent.scroll({ + left: x, + behavior: 'smooth', + }) + } + } }, [scrollElRef, itemXs, selectedPage, styles]) const onPressItem = useCallback( @@ -78,6 +130,7 @@ export function TabBar({ (itemRefs.current[i] = node)} onLayout={e => onItemLayout(e, i)} style={styles.item} hoverStyle={pal.viewLight} @@ -94,6 +147,7 @@ export function TabBar({ ) })} + ) } @@ -117,6 +171,13 @@ const desktopStyles = StyleSheet.create({ borderBottomWidth: 3, borderBottomColor: 'transparent', }, + outerBottomBorder: { + position: 'absolute', + left: 0, + right: 0, + bottom: -1, + borderBottomWidth: 1, + }, }) const mobileStyles = StyleSheet.create({ @@ -137,4 +198,11 @@ const mobileStyles = StyleSheet.create({ borderBottomWidth: 3, borderBottomColor: 'transparent', }, + outerBottomBorder: { + position: 'absolute', + left: 0, + right: 0, + bottom: -1, + borderBottomWidth: 1, + }, }) diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 2c90e33f..01b8a954 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -20,12 +20,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { const setMode = useSetMinimalShellMode() const startDragOffset = useSharedValue(null) const startMode = useSharedValue(null) + const didJustRestoreScroll = useSharedValue(false) useEffect(() => { if (isWeb) { return listenToForcedWindowScroll(() => { startDragOffset.value = null startMode.value = null + didJustRestoreScroll.value = true }) } }) @@ -86,6 +88,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { mode.value = newValue } } else { + if (didJustRestoreScroll.value) { + didJustRestoreScroll.value = false + // Don't hide/show navbar based on scroll restoratoin. + return + } // On the web, we don't try to follow the drag because we don't know when it ends. // Instead, show/hide immediately based on whether we're scrolling up or down. const dy = e.contentOffset.y - (startDragOffset.value ?? 0) @@ -98,7 +105,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { } } }, - [headerHeight, mode, setMode, startDragOffset, startMode], + [ + headerHeight, + mode, + setMode, + startDragOffset, + startMode, + didJustRestoreScroll, + ], ) return (