about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-12-10 14:52:30 -0600
committerGitHub <noreply@github.com>2024-12-10 20:52:30 +0000
commite052f5e198603246cb031e00d9cadc2ae4bb140d (patch)
treeaceffa2b71bfdc9c632ff8eaa84d68c807fd5655
parentf34e8d8cdfcab16165c94d8c96084e9cd4338d91 (diff)
downloadvoidsky-e052f5e198603246cb031e00d9cadc2ae4bb140d.tar.zst
Refactor sidebar (#6971)
* Refactor RightNav

(cherry picked from commit 96bb02acfd2d7452df18a0e7410e6a7169a583ed)

* Better gutter handling

* Clean up styles

* Memoize breakpoints

* Format

* Comment

* Loosen spacing, handle overflow, smaller text to match prod

* Fix circular imports on native

* Return 0 instead of undefined for easier calculations

* Re-assign

* Fix

* Port over fix from subs/base

* Space out right nav feeds, widen sidebar to match prod

* Fix lost padding on home header

* Fix perf by not actually linking to new URL

* Remove underline on focus

* Foramt

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
-rw-r--r--src/alf/breakpoints.ts28
-rw-r--r--src/alf/index.tsx15
-rw-r--r--src/alf/util/useGutterStyles.ts21
-rw-r--r--src/alf/util/useGutters.ts66
-rw-r--r--src/components/Layout/Header/index.tsx6
-rw-r--r--src/components/Link.tsx9
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx6
-rw-r--r--src/view/shell/desktop/Feeds.tsx102
-rw-r--r--src/view/shell/desktop/RightNav.tsx197
-rw-r--r--src/view/shell/desktop/Search.tsx4
10 files changed, 228 insertions, 226 deletions
diff --git a/src/alf/breakpoints.ts b/src/alf/breakpoints.ts
new file mode 100644
index 000000000..934585644
--- /dev/null
+++ b/src/alf/breakpoints.ts
@@ -0,0 +1,28 @@
+import {useMemo} from 'react'
+import {useMediaQuery} from 'react-responsive'
+
+export type Breakpoint = 'gtPhone' | 'gtMobile' | 'gtTablet'
+
+export function useBreakpoints(): Record<Breakpoint, boolean> & {
+  activeBreakpoint: Breakpoint | undefined
+} {
+  const gtPhone = useMediaQuery({minWidth: 500})
+  const gtMobile = useMediaQuery({minWidth: 800})
+  const gtTablet = useMediaQuery({minWidth: 1300})
+  return useMemo(() => {
+    let active: Breakpoint | undefined
+    if (gtTablet) {
+      active = 'gtTablet'
+    } else if (gtMobile) {
+      active = 'gtMobile'
+    } else if (gtPhone) {
+      active = 'gtPhone'
+    }
+    return {
+      activeBreakpoint: active,
+      gtPhone,
+      gtMobile,
+      gtTablet,
+    }
+  }, [gtPhone, gtMobile, gtTablet])
+}
diff --git a/src/alf/index.tsx b/src/alf/index.tsx
index a96803c56..5443669c7 100644
--- a/src/alf/index.tsx
+++ b/src/alf/index.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {useMediaQuery} from 'react-responsive'
 
 import {
   computeFontScaleMultiplier,
@@ -14,13 +13,14 @@ import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
 import {Device} from '#/storage'
 
 export {atoms} from '#/alf/atoms'
+export * from '#/alf/breakpoints'
 export * from '#/alf/fonts'
 export * as tokens from '#/alf/tokens'
 export * from '#/alf/types'
 export * from '#/alf/util/flatten'
 export * from '#/alf/util/platform'
 export * from '#/alf/util/themeSelector'
-export * from '#/alf/util/useGutterStyles'
+export * from '#/alf/util/useGutters'
 
 export type Alf = {
   themeName: ThemeName
@@ -142,14 +142,3 @@ export function useTheme(theme?: ThemeName) {
     return theme ? alf.themes[theme] : alf.theme
   }, [theme, alf])
 }
-
-export function useBreakpoints() {
-  const gtPhone = useMediaQuery({minWidth: 500})
-  const gtMobile = useMediaQuery({minWidth: 800})
-  const gtTablet = useMediaQuery({minWidth: 1300})
-  return {
-    gtPhone,
-    gtMobile,
-    gtTablet,
-  }
-}
diff --git a/src/alf/util/useGutterStyles.ts b/src/alf/util/useGutterStyles.ts
deleted file mode 100644
index 64b246fdd..000000000
--- a/src/alf/util/useGutterStyles.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react'
-
-import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
-
-export function useGutterStyles({
-  top,
-  bottom,
-}: {
-  top?: boolean
-  bottom?: boolean
-} = {}) {
-  const {gtMobile} = useBreakpoints()
-  return React.useMemo<ViewStyleProp['style']>(() => {
-    return [
-      a.px_lg,
-      top && a.pt_md,
-      bottom && a.pb_md,
-      gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
-    ]
-  }, [gtMobile, top, bottom])
-}
diff --git a/src/alf/util/useGutters.ts b/src/alf/util/useGutters.ts
new file mode 100644
index 000000000..57dd4e80b
--- /dev/null
+++ b/src/alf/util/useGutters.ts
@@ -0,0 +1,66 @@
+import React from 'react'
+
+import {Breakpoint, useBreakpoints} from '#/alf/breakpoints'
+import * as tokens from '#/alf/tokens'
+
+type Gutter = 'compact' | 'base' | 'wide' | 0
+
+const gutters: Record<
+  Exclude<Gutter, 0>,
+  Record<Breakpoint | 'default', number>
+> = {
+  compact: {
+    default: tokens.space.sm,
+    gtPhone: tokens.space.sm,
+    gtMobile: tokens.space.md,
+    gtTablet: tokens.space.md,
+  },
+  base: {
+    default: tokens.space.lg,
+    gtPhone: tokens.space.lg,
+    gtMobile: tokens.space.xl,
+    gtTablet: tokens.space.xl,
+  },
+  wide: {
+    default: tokens.space.xl,
+    gtPhone: tokens.space.xl,
+    gtMobile: tokens.space._3xl,
+    gtTablet: tokens.space._3xl,
+  },
+}
+
+type Gutters = {
+  paddingTop: number
+  paddingRight: number
+  paddingBottom: number
+  paddingLeft: number
+}
+
+export function useGutters([all]: [Gutter]): Gutters
+export function useGutters([vertical, horizontal]: [Gutter, Gutter]): Gutters
+export function useGutters([top, right, bottom, left]: [
+  Gutter,
+  Gutter,
+  Gutter,
+  Gutter,
+]): Gutters
+export function useGutters([top, right, bottom, left]: Gutter[]) {
+  const {activeBreakpoint} = useBreakpoints()
+  if (right === undefined) {
+    right = bottom = left = top
+  } else if (bottom === undefined) {
+    bottom = top
+    left = right
+  }
+  return React.useMemo(() => {
+    return {
+      paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'],
+      paddingRight:
+        right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'],
+      paddingBottom:
+        bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'],
+      paddingLeft:
+        left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'],
+    }
+  }, [activeBreakpoint, top, right, bottom, left])
+}
diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx
index a35a09537..321f7201f 100644
--- a/src/components/Layout/Header/index.tsx
+++ b/src/components/Layout/Header/index.tsx
@@ -13,7 +13,7 @@ import {
   platform,
   TextStyleProp,
   useBreakpoints,
-  useGutterStyles,
+  useGutters,
   useTheme,
 } from '#/alf'
 import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
@@ -34,7 +34,7 @@ export function Outer({
   noBottomBorder?: boolean
 }) {
   const t = useTheme()
-  const gutter = useGutterStyles()
+  const gutters = useGutters([0, 'base'])
   const {gtMobile} = useBreakpoints()
   const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
 
@@ -46,7 +46,7 @@ export function Outer({
         a.flex_row,
         a.align_center,
         a.gap_sm,
-        gutter,
+        gutters,
         platform({
           native: [a.pb_sm, a.pt_xs],
           web: [a.py_sm],
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index a5203b252..3cd593a10 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -237,7 +237,9 @@ export function Link({
 }
 
 export type InlineLinkProps = React.PropsWithChildren<
-  BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
+  BaseLinkProps &
+    TextStyleProp &
+    Pick<TextProps, 'selectable' | 'numberOfLines'>
 > &
   Pick<ButtonProps, 'label'> & {
     disableUnderline?: boolean
@@ -273,7 +275,6 @@ export function InlineLinkText({
     onIn: onHoverIn,
     onOut: onHoverOut,
   } = useInteractionState()
-  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
   const flattenedStyle = flatten(style) || {}
 
   return (
@@ -284,7 +285,7 @@ export function InlineLinkText({
       {...rest}
       style={[
         {color: t.palette.primary_500},
-        (hovered || focused) &&
+        hovered &&
           !disableUnderline && {
             ...web({
               outline: 0,
@@ -298,8 +299,6 @@ export function InlineLinkText({
       role="link"
       onPress={download ? undefined : onPress}
       onLongPress={onLongPress}
-      onFocus={onFocus}
-      onBlur={onBlur}
       onMouseEnter={onHoverIn}
       onMouseLeave={onHoverOut}
       accessibilityRole="link"
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index 1dc67b6c3..7a8a7671d 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -10,7 +10,7 @@ import {useSession} from '#/state/session'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
 import {Logo} from '#/view/icons/Logo'
-import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf'
+import {atoms as a, useBreakpoints, useGutters, useTheme} from '#/alf'
 import {ButtonIcon} from '#/components/Button'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
 import * as Layout from '#/components/Layout'
@@ -41,14 +41,14 @@ function HomeHeaderLayoutDesktopAndTablet({
   const {hasSession} = useSession()
   const {_} = useLingui()
   const kawaii = useKawaiiMode()
-  const gutter = useGutterStyles()
+  const gutters = useGutters([0, 'base'])
 
   return (
     <>
       {hasSession && (
         <Layout.Center>
           <View
-            style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}>
+            style={[a.flex_row, a.align_center, gutters, a.pt_md, t.atoms.bg]}>
             <View style={{width: 34}} />
             <View style={[a.flex_1, a.align_center, a.justify_center]}>
               <Logo width={kawaii ? 60 : 28} />
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 383d8f953..83b5420ce 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -1,18 +1,18 @@
-import {StyleSheet, View} from 'react-native'
+import {View} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation, useNavigationState} from '@react-navigation/native'
 
-import {usePalette} from '#/lib/hooks/usePalette'
 import {getCurrentRoute} from '#/lib/routes/helpers'
 import {NavigationProp} from '#/lib/routes/types'
 import {emitSoftReset} from '#/state/events'
 import {usePinnedFeedsInfos} from '#/state/queries/feed'
 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
-import {TextLink} from '#/view/com/util/Link'
+import {atoms as a, useTheme, web} from '#/alf'
+import {createStaticClick, InlineLinkText} from '#/components/Link'
 
 export function DesktopFeeds() {
-  const pal = usePalette('default')
+  const t = useTheme()
   const {_} = useLingui()
   const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
   const selectedFeed = useSelectedFeed()
@@ -24,76 +24,60 @@ export function DesktopFeeds() {
     }
     return getCurrentRoute(state)
   })
+
   if (!pinnedFeedInfos) {
     return null
   }
+
   return (
-    <View style={[styles.container, pal.view]}>
+    <View
+      style={[
+        a.flex_1,
+        web({
+          gap: 10,
+          /*
+           * Small padding prevents overflow prior to actually overflowing the
+           * height of the screen with lots of feeds.
+           */
+          paddingVertical: 2,
+          overflowY: 'auto',
+        }),
+      ]}>
       {pinnedFeedInfos.map(feedInfo => {
         const feed = feedInfo.feedDescriptor
+        const current = route.name === 'Home' && feed === selectedFeed
+
         return (
-          <FeedItem
-            key={feed}
-            href={'/?' + new URLSearchParams([['feed', feed]])}
-            title={feedInfo.displayName}
-            current={route.name === 'Home' && feed === selectedFeed}
-            onPress={() => {
+          <InlineLinkText
+            key={feedInfo.uri}
+            label={feedInfo.displayName}
+            {...createStaticClick(() => {
               setSelectedFeed(feed)
               navigation.navigate('Home')
               if (route.name === 'Home' && feed === selectedFeed) {
                 emitSoftReset()
               }
-            }}
-          />
+            })}
+            style={[
+              a.text_md,
+              a.leading_snug,
+              current
+                ? [a.font_heavy, t.atoms.text]
+                : [t.atoms.text_contrast_medium],
+            ]}
+            numberOfLines={1}>
+            {feedInfo.displayName}
+          </InlineLinkText>
         )
       })}
-      <View style={{paddingTop: 8, paddingBottom: 6}}>
-        <TextLink
-          type="lg"
-          href="/feeds"
-          text={_(msg`More feeds`)}
-          style={[pal.link]}
-        />
-      </View>
-    </View>
-  )
-}
 
-function FeedItem({
-  title,
-  href,
-  current,
-  onPress,
-}: {
-  title: string
-  href: string
-  current: boolean
-  onPress: () => void
-}) {
-  const pal = usePalette('default')
-  return (
-    <View style={{paddingVertical: 6}}>
-      <TextLink
-        type="xl"
-        href={href}
-        text={title}
-        onPress={onPress}
-        style={[
-          current ? pal.text : pal.textLight,
-          {letterSpacing: 0.15, fontWeight: current ? '600' : '400'},
-        ]}
-      />
+      <InlineLinkText
+        to="/feeds"
+        label={_(msg`More feeds`)}
+        style={[a.text_md, a.leading_snug]}
+        numberOfLines={1}>
+        {_(msg`More feeds`)}
+      </InlineLinkText>
     </View>
   )
 }
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    // @ts-ignore web only -prf
-    overflowY: 'auto',
-    width: 300,
-    paddingHorizontal: 12,
-    paddingVertical: 18,
-  },
-})
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 7814f3548..895d16021 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -1,26 +1,24 @@
-import {StyleSheet, View} from 'react-native'
+import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
-import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {s} from '#/lib/styles'
 import {useKawaiiMode} from '#/state/preferences/kawaii'
 import {useSession} from '#/state/session'
-import {TextLink} from '#/view/com/util/Link'
-import {Text} from '#/view/com/util/text/Text'
-import {atoms as a} from '#/alf'
+import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
+import {DesktopSearch} from '#/view/shell/desktop/Search'
+import {atoms as a, useGutters, useTheme, web} from '#/alf'
+import {InlineLinkText} from '#/components/Link'
 import {ProgressGuideList} from '#/components/ProgressGuide/List'
-import {DesktopFeeds} from './Feeds'
-import {DesktopSearch} from './Search'
+import {Text} from '#/components/Typography'
 
 export function DesktopRightNav({routeName}: {routeName: string}) {
-  const pal = usePalette('default')
+  const t = useTheme()
   const {_} = useLingui()
   const {hasSession, currentAccount} = useSession()
-
   const kawaii = useKawaiiMode()
+  const gutters = useGutters(['base', 0, 'base', 'wide'])
 
   const {isTablet} = useWebMediaQueries()
   if (isTablet) {
@@ -28,122 +26,81 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
   }
 
   return (
-    <View style={[a.px_xl, styles.rightNav]}>
-      <View style={{paddingVertical: 20}}>
-        {routeName === 'Search' ? (
-          <View style={{marginBottom: 18}}>
+    <View
+      style={[
+        gutters,
+        web({
+          position: 'fixed',
+          left: '50%',
+          transform: [
+            {
+              translateX: 300,
+            },
+            ...a.scrollbar_offset.transform,
+          ],
+          width: 300 + gutters.paddingLeft,
+          maxHeight: '100%',
+          overflowY: 'auto',
+        }),
+      ]}>
+      {routeName !== 'Search' && (
+        <View style={[a.pb_lg]}>
+          <DesktopSearch />
+        </View>
+      )}
+      {hasSession && (
+        <>
+          <ProgressGuideList style={[a.pb_xl]} />
+          <View
+            style={[a.pb_lg, a.mb_lg, a.border_b, t.atoms.border_contrast_low]}>
             <DesktopFeeds />
           </View>
-        ) : (
-          <>
-            <DesktopSearch />
+        </>
+      )}
 
-            {hasSession && (
-              <>
-                <ProgressGuideList style={[{marginTop: 22, marginBottom: 8}]} />
-                <View style={[pal.border, styles.desktopFeedsContainer]}>
-                  <DesktopFeeds />
-                </View>
-              </>
-            )}
+      <Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
+        {hasSession && (
+          <>
+            <InlineLinkText
+              to={FEEDBACK_FORM_URL({
+                email: currentAccount?.email,
+                handle: currentAccount?.handle,
+              })}
+              label={_(msg`Feedback`)}>
+              {_(msg`Feedback`)}
+            </InlineLinkText>
+            {' • '}
           </>
         )}
+        <InlineLinkText
+          to="https://bsky.social/about/support/privacy-policy"
+          label={_(msg`Privacy`)}>
+          {_(msg`Privacy`)}
+        </InlineLinkText>
+        {' • '}
+        <InlineLinkText
+          to="https://bsky.social/about/support/tos"
+          label={_(msg`Terms`)}>
+          {_(msg`Terms`)}
+        </InlineLinkText>
+        {' • '}
+        <InlineLinkText label={_(msg`Help`)} to={HELP_DESK_URL}>
+          {_(msg`Help`)}
+        </InlineLinkText>
+      </Text>
 
-        <View
-          style={[
-            styles.message,
-            {
-              paddingTop: hasSession ? 0 : 18,
-            },
-          ]}>
-          <View style={[{flexWrap: 'wrap'}, s.flexRow, a.gap_xs]}>
-            {hasSession && (
-              <>
-                <TextLink
-                  type="md"
-                  style={pal.link}
-                  href={FEEDBACK_FORM_URL({
-                    email: currentAccount?.email,
-                    handle: currentAccount?.handle,
-                  })}
-                  text={_(msg`Feedback`)}
-                />
-                <Text type="md" style={pal.textLight}>
-                  &middot;
-                </Text>
-              </>
-            )}
-            <TextLink
-              type="md"
-              style={pal.link}
-              href="https://bsky.social/about/support/privacy-policy"
-              text={_(msg`Privacy`)}
-            />
-            <Text type="md" style={pal.textLight}>
-              &middot;
-            </Text>
-            <TextLink
-              type="md"
-              style={pal.link}
-              href="https://bsky.social/about/support/tos"
-              text={_(msg`Terms`)}
-            />
-            <Text type="md" style={pal.textLight}>
-              &middot;
-            </Text>
-            <TextLink
-              type="md"
-              style={pal.link}
-              href={HELP_DESK_URL}
-              text={_(msg`Help`)}
-            />
-          </View>
-          {kawaii && (
-            <Text type="md" style={[pal.textLight, {marginTop: 12}]}>
-              <Trans>
-                Logo by{' '}
-                <TextLink
-                  type="md"
-                  href="/profile/sawaratsuki.bsky.social"
-                  text="@sawaratsuki.bsky.social"
-                  style={pal.link}
-                />
-              </Trans>
-            </Text>
-          )}
-        </View>
-      </View>
+      {kawaii && (
+        <Text style={[t.atoms.text_contrast_medium, {marginTop: 12}]}>
+          <Trans>
+            Logo by{' '}
+            <InlineLinkText
+              label={_(msg`Logo by @sawaratsuki.bsky.social`)}
+              to="/profile/sawaratsuki.bsky.social">
+              @sawaratsuki.bsky.social
+            </InlineLinkText>
+          </Trans>
+        </Text>
+      )}
     </View>
   )
 }
-
-const styles = StyleSheet.create({
-  rightNav: {
-    // @ts-ignore web only
-    position: 'fixed',
-    // @ts-ignore web only
-    left: '50%',
-    transform: [
-      {
-        translateX: 300,
-      },
-      ...a.scrollbar_offset.transform,
-    ],
-    maxHeight: '100%',
-    overflowY: 'auto',
-  },
-
-  message: {
-    paddingVertical: 18,
-    paddingHorizontal: 12,
-  },
-  messageLine: {
-    marginBottom: 10,
-  },
-  desktopFeedsContainer: {
-    borderTopWidth: StyleSheet.hairlineWidth,
-    borderBottomWidth: StyleSheet.hairlineWidth,
-    marginTop: 18,
-    marginBottom: 18,
-  },
-})
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 2780944f1..de3ccad39 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -225,12 +225,12 @@ export function DesktopSearch() {
 const styles = StyleSheet.create({
   container: {
     position: 'relative',
-    width: 300,
+    width: '100%',
   },
   resultsContainer: {
     marginTop: 10,
     flexDirection: 'column',
-    width: 300,
+    width: '100%',
     borderWidth: 1,
     borderRadius: 6,
   },