about summary refs log tree commit diff
path: root/src/view/com/pager
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/pager')
-rw-r--r--src/view/com/pager/DraggableScrollView.tsx15
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx11
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx74
-rw-r--r--src/view/com/pager/Pager.tsx125
-rw-r--r--src/view/com/pager/Pager.web.tsx99
-rw-r--r--src/view/com/pager/TabBar.tsx202
6 files changed, 271 insertions, 255 deletions
diff --git a/src/view/com/pager/DraggableScrollView.tsx b/src/view/com/pager/DraggableScrollView.tsx
new file mode 100644
index 000000000..4b7396eaa
--- /dev/null
+++ b/src/view/com/pager/DraggableScrollView.tsx
@@ -0,0 +1,15 @@
+import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView'
+import React, {ComponentProps} from 'react'
+import {ScrollView} from 'react-native'
+
+export const DraggableScrollView = React.forwardRef<
+  ScrollView,
+  ComponentProps<typeof ScrollView>
+>(function DraggableScrollView(props, ref) {
+  const {refs} = useDraggableScroll<ScrollView>({
+    outerRef: ref,
+    cursor: 'grab', // optional, default
+  })
+
+  return <ScrollView ref={refs} horizontal {...props} />
+})
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index 0fc1b7310..0df915950 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useMemo} from 'react'
 import {Animated, StyleSheet} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {TabBar} from 'view/com/pager/TabBar'
@@ -27,6 +27,10 @@ const FeedsTabBarDesktop = observer(
     props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
   ) => {
     const store = useStores()
+    const items = useMemo(
+      () => ['Following', ...store.me.savedFeeds.pinnedFeedNames],
+      [store.me.savedFeeds.pinnedFeedNames],
+    )
     const pal = usePalette('default')
     const interp = useAnimatedValue(0)
 
@@ -44,13 +48,14 @@ const FeedsTabBarDesktop = observer(
         {translateY: Animated.multiply(interp, -100)},
       ],
     }
+
     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]}>
         <TabBar
+          key={items.join(',')}
           {...props}
-          items={['Following', "What's hot"]}
-          indicatorPosition="bottom"
+          items={items}
           indicatorColor={pal.colors.link}
         />
       </Animated.View>
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index b42ffe726..9c7138815 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -1,12 +1,17 @@
-import React from 'react'
-import {Animated, StyleSheet, TouchableOpacity} from 'react-native'
+import React, {useMemo} from 'react'
+import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {TabBar} from 'view/com/pager/TabBar'
 import {RenderTabBarFnProps} from 'view/com/pager/Pager'
-import {UserAvatar} from '../util/UserAvatar'
 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'
+import {CogIcon} from 'lib/icons'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {s} from 'lib/styles'
 
 export const FeedsTabBar = observer(
   (
@@ -28,25 +33,51 @@ export const FeedsTabBar = observer(
       transform: [{translateY: Animated.multiply(interp, -100)}],
     }
 
+    const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
+
     const onPressAvi = React.useCallback(() => {
       store.shell.openDrawer()
     }, [store])
 
+    const items = useMemo(
+      () => ['Following', ...store.me.savedFeeds.pinnedFeedNames],
+      [store.me.savedFeeds.pinnedFeedNames],
+    )
+
     return (
       <Animated.View style={[pal.view, pal.border, styles.tabBar, transform]}>
-        <TouchableOpacity
-          testID="viewHeaderDrawerBtn"
-          style={styles.tabBarAvi}
-          onPress={onPressAvi}
-          accessibilityRole="button"
-          accessibilityLabel="Menu"
-          accessibilityHint="Access navigation links and settings">
-          <UserAvatar avatar={store.me.avatar} size={30} />
-        </TouchableOpacity>
+        <View style={[pal.view, styles.topBar]}>
+          <View style={[pal.view]}>
+            <TouchableOpacity
+              testID="viewHeaderDrawerBtn"
+              onPress={onPressAvi}
+              accessibilityRole="button"
+              accessibilityLabel="Open navigation"
+              accessibilityHint="Access profile and other navigation links"
+              hitSlop={10}>
+              <FontAwesomeIcon
+                icon="bars"
+                size={18}
+                color={pal.colors.textLight}
+              />
+            </TouchableOpacity>
+          </View>
+          <Text style={[brandBlue, s.bold, styles.title]}>Bluesky</Text>
+          <View style={[pal.view]}>
+            <Link
+              href="/settings/saved-feeds"
+              hitSlop={10}
+              accessibilityRole="button"
+              accessibilityLabel="Edit Saved Feeds"
+              accessibilityHint="Opens screen to edit Saved Feeds">
+              <CogIcon size={21} strokeWidth={2} style={pal.textLight} />
+            </Link>
+          </View>
+        </View>
         <TabBar
+          key={items.join(',')}
           {...props}
-          items={['Following', "What's hot"]}
-          indicatorPosition="bottom"
+          items={items}
           indicatorColor={pal.colors.link}
         />
       </Animated.View>
@@ -61,13 +92,20 @@ const styles = StyleSheet.create({
     left: 0,
     right: 0,
     top: 0,
+    flexDirection: 'column',
+    alignItems: 'center',
+    borderBottomWidth: 1,
+  },
+  topBar: {
     flexDirection: 'row',
+    justifyContent: 'space-between',
     alignItems: 'center',
     paddingHorizontal: 18,
-    borderBottomWidth: 1,
+    paddingTop: 8,
+    paddingBottom: 2,
+    width: '100%',
   },
-  tabBarAvi: {
-    marginTop: 1,
-    marginRight: 18,
+  title: {
+    fontSize: 21,
   },
 })
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 34747db6d..e2c8bf6d2 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -1,16 +1,17 @@
-import React from 'react'
+import React, {forwardRef} from 'react'
 import {Animated, View} from 'react-native'
 import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {s} from 'lib/styles'
 
 export type PageSelectedEvent = PagerViewOnPageSelectedEvent
 const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
 
+export interface PagerRef {
+  setPage: (index: number) => void
+}
+
 export interface RenderTabBarFnProps {
   selectedPage: number
-  position: Animated.Value
-  offset: Animated.Value
   onSelect?: (index: number) => void
 }
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
@@ -22,68 +23,60 @@ interface Props {
   onPageSelected?: (index: number) => void
   testID?: string
 }
-export const Pager = ({
-  children,
-  tabBarPosition = 'top',
-  initialPage = 0,
-  renderTabBar,
-  onPageSelected,
-  testID,
-}: React.PropsWithChildren<Props>) => {
-  const [selectedPage, setSelectedPage] = React.useState(0)
-  const position = useAnimatedValue(0)
-  const offset = useAnimatedValue(0)
-  const pagerView = React.useRef<PagerView>()
+export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
+  (
+    {
+      children,
+      tabBarPosition = 'top',
+      initialPage = 0,
+      renderTabBar,
+      onPageSelected,
+      testID,
+    }: React.PropsWithChildren<Props>,
+    ref,
+  ) => {
+    const [selectedPage, setSelectedPage] = React.useState(0)
+    const pagerView = React.useRef<PagerView>()
 
-  const onPageSelectedInner = React.useCallback(
-    (e: PageSelectedEvent) => {
-      setSelectedPage(e.nativeEvent.position)
-      onPageSelected?.(e.nativeEvent.position)
-    },
-    [setSelectedPage, onPageSelected],
-  )
+    React.useImperativeHandle(ref, () => ({
+      setPage: (index: number) => pagerView.current?.setPage(index),
+    }))
 
-  const onTabBarSelect = React.useCallback(
-    (index: number) => {
-      pagerView.current?.setPage(index)
-    },
-    [pagerView],
-  )
+    const onPageSelectedInner = React.useCallback(
+      (e: PageSelectedEvent) => {
+        setSelectedPage(e.nativeEvent.position)
+        onPageSelected?.(e.nativeEvent.position)
+      },
+      [setSelectedPage, onPageSelected],
+    )
 
-  return (
-    <View testID={testID}>
-      {tabBarPosition === 'top' &&
-        renderTabBar({
-          selectedPage,
-          position,
-          offset,
-          onSelect: onTabBarSelect,
-        })}
-      <AnimatedPagerView
-        ref={pagerView}
-        style={s.h100pct}
-        initialPage={initialPage}
-        onPageSelected={onPageSelectedInner}
-        onPageScroll={Animated.event(
-          [
-            {
-              nativeEvent: {
-                position: position,
-                offset: offset,
-              },
-            },
-          ],
-          {useNativeDriver: true},
-        )}>
-        {children}
-      </AnimatedPagerView>
-      {tabBarPosition === 'bottom' &&
-        renderTabBar({
-          selectedPage,
-          position,
-          offset,
-          onSelect: onTabBarSelect,
-        })}
-    </View>
-  )
-}
+    const onTabBarSelect = React.useCallback(
+      (index: number) => {
+        pagerView.current?.setPage(index)
+      },
+      [pagerView],
+    )
+
+    return (
+      <View testID={testID}>
+        {tabBarPosition === 'top' &&
+          renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+          })}
+        <AnimatedPagerView
+          ref={pagerView}
+          style={s.h100pct}
+          initialPage={initialPage}
+          onPageSelected={onPageSelectedInner}>
+          {children}
+        </AnimatedPagerView>
+        {tabBarPosition === 'bottom' &&
+          renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+          })}
+      </View>
+    )
+  },
+)
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index 107497f6f..7be2b11ec 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -1,12 +1,9 @@
 import React from 'react'
-import {Animated, View} from 'react-native'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {View} from 'react-native'
 import {s} from 'lib/styles'
 
 export interface RenderTabBarFnProps {
   selectedPage: number
-  position: Animated.Value
-  offset: Animated.Value
   onSelect?: (index: number) => void
 }
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
@@ -17,53 +14,51 @@ interface Props {
   renderTabBar: RenderTabBarFn
   onPageSelected?: (index: number) => void
 }
-export const Pager = ({
-  children,
-  tabBarPosition = 'top',
-  initialPage = 0,
-  renderTabBar,
-  onPageSelected,
-}: React.PropsWithChildren<Props>) => {
-  const [selectedPage, setSelectedPage] = React.useState(initialPage)
-  const position = useAnimatedValue(0)
-  const offset = useAnimatedValue(0)
+export const Pager = React.forwardRef(
+  (
+    {
+      children,
+      tabBarPosition = 'top',
+      initialPage = 0,
+      renderTabBar,
+      onPageSelected,
+    }: React.PropsWithChildren<Props>,
+    ref,
+  ) => {
+    const [selectedPage, setSelectedPage] = React.useState(initialPage)
 
-  const onTabBarSelect = React.useCallback(
-    (index: number) => {
-      setSelectedPage(index)
-      onPageSelected?.(index)
-      Animated.timing(position, {
-        toValue: index,
-        duration: 200,
-        useNativeDriver: true,
-      }).start()
-    },
-    [setSelectedPage, onPageSelected, position],
-  )
+    React.useImperativeHandle(ref, () => ({
+      setPage: (index: number) => setSelectedPage(index),
+    }))
 
-  return (
-    <View>
-      {tabBarPosition === 'top' &&
-        renderTabBar({
-          selectedPage,
-          position,
-          offset,
-          onSelect: onTabBarSelect,
-        })}
-      {React.Children.map(children, (child, i) => (
-        <View
-          style={selectedPage === i ? undefined : s.hidden}
-          key={`page-${i}`}>
-          {child}
-        </View>
-      ))}
-      {tabBarPosition === 'bottom' &&
-        renderTabBar({
-          selectedPage,
-          position,
-          offset,
-          onSelect: onTabBarSelect,
-        })}
-    </View>
-  )
-}
+    const onTabBarSelect = React.useCallback(
+      (index: number) => {
+        setSelectedPage(index)
+        onPageSelected?.(index)
+      },
+      [setSelectedPage, onPageSelected],
+    )
+
+    return (
+      <View>
+        {tabBarPosition === 'top' &&
+          renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+          })}
+        {React.Children.map(children, (child, i) => (
+          <View
+            style={selectedPage === i ? undefined : s.hidden}
+            key={`page-${i}`}>
+            {child}
+          </View>
+        ))}
+        {tabBarPosition === 'bottom' &&
+          renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+          })}
+      </View>
+    )
+  },
+)
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index a0b72a93f..d7121fde9 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -1,22 +1,22 @@
-import React, {createRef, useState, useMemo, useRef} from 'react'
-import {Animated, StyleSheet, View} from 'react-native'
+import React, {
+  useRef,
+  createRef,
+  useMemo,
+  useEffect,
+  useState,
+  useCallback,
+} from 'react'
+import {StyleSheet, View, ScrollView} from 'react-native'
 import {Text} from '../util/text/Text'
 import {PressableWithHover} from '../util/PressableWithHover'
 import {usePalette} from 'lib/hooks/usePalette'
-import {isDesktopWeb} from 'platform/detection'
-
-interface Layout {
-  x: number
-  width: number
-}
+import {isDesktopWeb, isMobileWeb} from 'platform/detection'
+import {DraggableScrollView} from './DraggableScrollView'
 
 export interface TabBarProps {
   testID?: string
   selectedPage: number
   items: string[]
-  position: Animated.Value
-  offset: Animated.Value
-  indicatorPosition?: 'top' | 'bottom'
   indicatorColor?: string
   onSelect?: (index: number) => void
   onPressSelected?: () => void
@@ -26,105 +26,81 @@ export function TabBar({
   testID,
   selectedPage,
   items,
-  position,
-  offset,
-  indicatorPosition = 'bottom',
   indicatorColor,
   onSelect,
   onPressSelected,
 }: TabBarProps) {
   const pal = usePalette('default')
-  const [itemLayouts, setItemLayouts] = useState<Layout[]>(
-    items.map(() => ({x: 0, width: 0})),
-  )
+  const scrollElRef = useRef<ScrollView>(null)
+  const [itemXs, setItemXs] = useState<number[]>([])
   const itemRefs = useMemo(
     () => Array.from({length: items.length}).map(() => createRef<View>()),
     [items.length],
   )
-  const panX = Animated.add(position, offset)
-  const containerRef = useRef<View>(null)
+  const indicatorStyle = useMemo(
+    () => ({borderBottomColor: indicatorColor || pal.colors.link}),
+    [indicatorColor, pal],
+  )
 
-  const indicatorStyle = {
-    backgroundColor: indicatorColor || pal.colors.link,
-    bottom:
-      indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined,
-    top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined,
-    transform: [
-      {
-        translateX: panX.interpolate({
-          inputRange: items.map((_item, i) => i),
-          outputRange: itemLayouts.map(l => l.x + l.width / 2),
-        }),
-      },
-      {
-        scaleX: panX.interpolate({
-          inputRange: items.map((_item, i) => i),
-          outputRange: itemLayouts.map(l => l.width),
-        }),
-      },
-    ],
-  }
+  useEffect(() => {
+    scrollElRef.current?.scrollTo({x: itemXs[selectedPage] || 0})
+  }, [scrollElRef, itemXs, selectedPage])
+
+  const onPressItem = useCallback(
+    (index: number) => {
+      onSelect?.(index)
+      if (index === selectedPage) {
+        onPressSelected?.()
+      }
+    },
+    [onSelect, onPressSelected, selectedPage],
+  )
 
   const onLayout = React.useCallback(() => {
     const promises = []
     for (let i = 0; i < items.length; i++) {
       promises.push(
-        new Promise<Layout>(resolve => {
-          if (!containerRef.current || !itemRefs[i].current) {
-            return resolve({x: 0, width: 0})
+        new Promise<number>(resolve => {
+          if (!itemRefs[i].current) {
+            return resolve(0)
           }
 
-          itemRefs[i].current?.measureLayout(
-            containerRef.current,
-            (x: number, _y: number, width: number) => {
-              resolve({x, width})
-            },
-          )
+          itemRefs[i].current?.measure((x: number) => resolve(x))
         }),
       )
     }
-    Promise.all(promises).then((layouts: Layout[]) => {
-      setItemLayouts(layouts)
+    Promise.all(promises).then((Xs: number[]) => {
+      setItemXs(Xs)
     })
-  }, [containerRef, itemRefs, setItemLayouts, items.length])
-
-  const onPressItem = React.useCallback(
-    (index: number) => {
-      onSelect?.(index)
-      if (index === selectedPage) {
-        onPressSelected?.()
-      }
-    },
-    [onSelect, onPressSelected, selectedPage],
-  )
+  }, [itemRefs, setItemXs, items.length])
 
   return (
-    <View
-      testID={testID}
-      style={[pal.view, styles.outer]}
-      onLayout={onLayout}
-      ref={containerRef}>
-      <Animated.View style={[styles.indicator, indicatorStyle]} />
-      {items.map((item, i) => {
-        const selected = i === selectedPage
-        return (
-          <PressableWithHover
-            ref={itemRefs[i]}
-            key={item}
-            style={
-              indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
-            }
-            hoverStyle={pal.viewLight}
-            onPress={() => onPressItem(i)}>
-            <Text
-              type="xl-bold"
-              testID={testID ? `${testID}-${item}` : undefined}
-              style={selected ? pal.text : pal.textLight}>
-              {item}
-            </Text>
-          </PressableWithHover>
-        )
-      })}
+    <View testID={testID} style={[pal.view, styles.outer]}>
+      <DraggableScrollView
+        horizontal={true}
+        showsHorizontalScrollIndicator={false}
+        ref={scrollElRef}
+        contentContainerStyle={styles.contentContainer}
+        onLayout={onLayout}>
+        {items.map((item, i) => {
+          const selected = i === selectedPage
+          return (
+            <PressableWithHover
+              ref={itemRefs[i]}
+              key={item}
+              style={[styles.item, selected && indicatorStyle]}
+              hoverStyle={pal.viewLight}
+              onPress={() => onPressItem(i)}>
+              <Text
+                type={isDesktopWeb ? 'xl-bold' : 'lg-bold'}
+                testID={testID ? `${testID}-${item}` : undefined}
+                style={selected ? pal.text : pal.textLight}>
+                {item}
+              </Text>
+            </PressableWithHover>
+          )
+        })}
+      </DraggableScrollView>
     </View>
   )
 }
@@ -133,45 +109,39 @@ const styles = isDesktopWeb
   ? StyleSheet.create({
       outer: {
         flexDirection: 'row',
-        paddingHorizontal: 18,
+        width: 598,
       },
-      itemTop: {
-        paddingTop: 16,
-        paddingBottom: 14,
-        paddingHorizontal: 12,
+      contentContainer: {
+        columnGap: 8,
+        marginLeft: 14,
+        paddingRight: 14,
+        backgroundColor: 'transparent',
       },
-      itemBottom: {
+      item: {
         paddingTop: 14,
-        paddingBottom: 16,
-        paddingHorizontal: 12,
-      },
-      indicator: {
-        position: 'absolute',
-        left: 0,
-        width: 1,
-        height: 3,
-        zIndex: 1,
+        paddingBottom: 12,
+        paddingHorizontal: 10,
+        borderBottomWidth: 3,
+        borderBottomColor: 'transparent',
       },
     })
   : StyleSheet.create({
       outer: {
+        flex: 1,
         flexDirection: 'row',
-        paddingHorizontal: 14,
+        backgroundColor: 'transparent',
       },
-      itemTop: {
+      contentContainer: {
+        columnGap: isMobileWeb ? 0 : 20,
+        marginLeft: isMobileWeb ? 0 : 18,
+        paddingRight: isMobileWeb ? 0 : 36,
+        backgroundColor: 'transparent',
+      },
+      item: {
         paddingTop: 10,
         paddingBottom: 10,
-        marginRight: 24,
-      },
-      itemBottom: {
-        paddingTop: 8,
-        paddingBottom: 12,
-        marginRight: 24,
-      },
-      indicator: {
-        position: 'absolute',
-        left: 0,
-        width: 1,
-        height: 3,
+        paddingHorizontal: isMobileWeb ? 8 : 0,
+        borderBottomWidth: 3,
+        borderBottomColor: 'transparent',
       },
     })