about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/view/com/feeds/FeedPage.tsx70
-rw-r--r--src/view/com/home/HomeHeader.tsx11
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx46
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx1
-rw-r--r--src/view/com/pager/TabBar.tsx78
-rw-r--r--src/view/com/util/MainScrollProvider.tsx16
6 files changed, 134 insertions, 88 deletions
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 2aacdb89d..e6b5d1fb6 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 (
-        <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={
-              <>
-                Bluesky{' '}
-                {hasNew && (
-                  <View
-                    style={{
-                      top: -8,
-                      backgroundColor: colors.blue3,
-                      width: 8,
-                      height: 8,
-                      borderRadius: 4,
-                    }}
-                  />
-                )}
-              </>
-            }
-            onPress={emitSoftReset}
-          />
-          {hasSession && (
-            <TextLink
-              type="title-lg"
-              href="/settings/following-feed"
-              style={{fontWeight: 'bold'}}
-              accessibilityLabel={_(msg`Feed Preferences`)}
-              accessibilityHint=""
-              text={
-                <FontAwesomeIcon
-                  icon="sliders"
-                  style={pal.textLight as FontAwesomeIconStyle}
-                />
-              }
-            />
-          )}
-        </View>
-      )
-    }
-    return <></>
-  }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, hasSession])
-
   return (
     <View testID={testID} style={s.h100pct}>
       <MainScrollProvider>
@@ -171,7 +106,6 @@ export function FeedPage({
           onHasNew={setHasNew}
           renderEmptyState={renderEmptyState}
           renderEndOfFeed={renderEndOfFeed}
-          ListHeaderComponent={ListHeaderComponent}
           headerOffset={headerOffset}
         />
       </MainScrollProvider>
diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
index 5ffa31f39..3df3858ba 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'
@@ -12,16 +11,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
 export function HomeHeader(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
-  const {isDesktop} = useWebMediaQueries()
-  if (isDesktop) {
-    return null
-  }
-  return <HomeHeaderInner {...props} />
-}
-
-export function HomeHeaderInner(
-  props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
-) {
   const navigation = useNavigation<NavigationProp>()
   const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
   const pal = usePalette('default')
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index 47cb00235..fbb55e6bc 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
       }}>
+      <View style={[pal.view, styles.topBar]}>
+        <TextLink
+          type="title-lg"
+          href="/settings/following-feed"
+          accessibilityLabel={_(msg`Following Feed Preferences`)}
+          accessibilityHint=""
+          text={
+            <FontAwesomeIcon
+              icon="sliders"
+              style={pal.textLight as FontAwesomeIconStyle}
+            />
+          }
+        />
+        <Logo width={28} />
+        <Link
+          href="/settings/saved-feeds"
+          hitSlop={10}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Edit Saved Feeds`)}
+          accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
+          <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
+        </Link>
+      </View>
       {children}
     </Animated.View>
   )
 }
 
 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 6c4b911f0..f51efb7b4 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 3204bb23e..ff8acd60c 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<ScrollView>(null)
+  const itemRefs = useRef<Array<Element>>([])
   const [itemXs, setItemXs] = useState<number[]>([])
   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({
             <PressableWithHover
               testID={`${testID}-selector-${i}`}
               key={`${item}-${i}`}
+              ref={node => (itemRefs.current[i] = node)}
               onLayout={e => onItemLayout(e, i)}
               style={styles.item}
               hoverStyle={pal.viewLight}
@@ -94,6 +147,7 @@ export function TabBar({
           )
         })}
       </DraggableScrollView>
+      <View style={[pal.border, styles.outerBottomBorder]} />
     </View>
   )
 }
@@ -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 2c90e33ff..01b8a954d 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<number | null>(null)
   const startMode = useSharedValue<number | null>(null)
+  const didJustRestoreScroll = useSharedValue<boolean>(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 (