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 (