about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-02-08 20:37:08 +0000
committerGitHub <noreply@github.com>2024-02-08 12:37:08 -0800
commit06f81d69486f2e6e8ffd65da5a4bfdcd7d8a2655 (patch)
tree041b9ddf0c44c871c6de14e96a65371c9c6c416d /src
parent80c482b026ad956e3a2bbfba864821a736e413f6 (diff)
downloadvoidsky-06f81d69486f2e6e8ffd65da5a4bfdcd7d8a2655.tar.zst
Keep pager feeds in sync with the right pane (#2775)
* Hoist selected feed state

* Seed state from params

* Refine and fix logic

* Fix scroll restoration

* Soft reset on second click
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx25
-rw-r--r--src/App.web.tsx25
-rw-r--r--src/state/shell/selected-feed.tsx61
-rw-r--r--src/view/com/pager/Pager.web.tsx2
-rw-r--r--src/view/screens/Home.tsx31
-rw-r--r--src/view/shell/desktop/Feeds.tsx67
6 files changed, 148 insertions, 63 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 50a80d9fe..1284154f3 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -32,6 +32,7 @@ import {Provider as MutedThreadsProvider} from 'state/muted-threads'
 import {Provider as InvitesStateProvider} from 'state/invites'
 import {Provider as PrefsStateProvider} from 'state/preferences'
 import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
+import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
 import I18nProvider from './locale/i18nProvider'
 import {
   Provider as SessionProvider,
@@ -72,17 +73,19 @@ function InnerApp() {
             // Resets the entire tree below when it changes:
             key={currentAccount?.did}>
             <LoggedOutViewProvider>
-              <UnreadNotifsProvider>
-                <ThemeProvider theme={theme}>
-                  {/* All components should be within this provider */}
-                  <RootSiblingParent>
-                    <GestureHandlerRootView style={s.h100pct}>
-                      <TestCtrls />
-                      <Shell />
-                    </GestureHandlerRootView>
-                  </RootSiblingParent>
-                </ThemeProvider>
-              </UnreadNotifsProvider>
+              <SelectedFeedProvider>
+                <UnreadNotifsProvider>
+                  <ThemeProvider theme={theme}>
+                    {/* All components should be within this provider */}
+                    <RootSiblingParent>
+                      <GestureHandlerRootView style={s.h100pct}>
+                        <TestCtrls />
+                        <Shell />
+                      </GestureHandlerRootView>
+                    </RootSiblingParent>
+                  </ThemeProvider>
+                </UnreadNotifsProvider>
+              </SelectedFeedProvider>
             </LoggedOutViewProvider>
           </React.Fragment>
         </Splash>
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 9d1bd3506..f10bb194d 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -22,6 +22,7 @@ import {Provider as MutedThreadsProvider} from 'state/muted-threads'
 import {Provider as InvitesStateProvider} from 'state/invites'
 import {Provider as PrefsStateProvider} from 'state/preferences'
 import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
+import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
 import I18nProvider from './locale/i18nProvider'
 import {
   Provider as SessionProvider,
@@ -52,17 +53,19 @@ function InnerApp() {
         // Resets the entire tree below when it changes:
         key={currentAccount?.did}>
         <LoggedOutViewProvider>
-          <UnreadNotifsProvider>
-            <ThemeProvider theme={theme}>
-              {/* All components should be within this provider */}
-              <RootSiblingParent>
-                <SafeAreaProvider>
-                  <Shell />
-                </SafeAreaProvider>
-              </RootSiblingParent>
-              <ToastContainer />
-            </ThemeProvider>
-          </UnreadNotifsProvider>
+          <SelectedFeedProvider>
+            <UnreadNotifsProvider>
+              <ThemeProvider theme={theme}>
+                {/* All components should be within this provider */}
+                <RootSiblingParent>
+                  <SafeAreaProvider>
+                    <Shell />
+                  </SafeAreaProvider>
+                </RootSiblingParent>
+                <ToastContainer />
+              </ThemeProvider>
+            </UnreadNotifsProvider>
+          </SelectedFeedProvider>
         </LoggedOutViewProvider>
       </React.Fragment>
     </Alf>
diff --git a/src/state/shell/selected-feed.tsx b/src/state/shell/selected-feed.tsx
new file mode 100644
index 000000000..a05d8661b
--- /dev/null
+++ b/src/state/shell/selected-feed.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import * as persisted from '#/state/persisted'
+import {isWeb} from '#/platform/detection'
+
+type StateContext = string
+type SetContext = (v: string) => void
+
+const stateContext = React.createContext<StateContext>('home')
+const setContext = React.createContext<SetContext>((_: string) => {})
+
+function getInitialFeed() {
+  if (isWeb) {
+    if (window.location.pathname === '/') {
+      const params = new URLSearchParams(window.location.search)
+      const feedFromUrl = params.get('feed')
+      if (feedFromUrl) {
+        // If explicitly booted from a link like /?feed=..., prefer that.
+        return feedFromUrl
+      }
+    }
+    const feedFromSession = sessionStorage.getItem('lastSelectedHomeFeed')
+    if (feedFromSession) {
+      // Fall back to a previously chosen feed for this browser tab.
+      return feedFromSession
+    }
+  }
+  const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
+  if (feedFromPersisted) {
+    // Fall back to the last chosen one across all tabs.
+    return feedFromPersisted
+  }
+  return 'home'
+}
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [state, setState] = React.useState(getInitialFeed)
+
+  const saveState = React.useCallback((feed: string) => {
+    setState(feed)
+    if (isWeb) {
+      try {
+        sessionStorage.setItem('lastSelectedHomeFeed', feed)
+      } catch {}
+    }
+    persisted.write('lastSelectedHomeFeed', feed)
+  }, [])
+
+  return (
+    <stateContext.Provider value={state}>
+      <setContext.Provider value={saveState}>{children}</setContext.Provider>
+    </stateContext.Provider>
+  )
+}
+
+export function useSelectedFeed() {
+  return React.useContext(stateContext)
+}
+
+export function useSetSelectedFeed() {
+  return React.useContext(setContext)
+}
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index d7113bb05..42982ef7f 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -31,7 +31,7 @@ export const Pager = React.forwardRef(function PagerImpl(
   const anchorRef = React.useRef(null)
 
   React.useImperativeHandle(ref, () => ({
-    setPage: (index: number) => setSelectedPage(index),
+    setPage: (index: number) => onTabBarSelect(index),
   }))
 
   const onTabBarSelect = React.useCallback(
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 7228a9e1b..cb2abf1bc 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -7,7 +7,7 @@ import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
 import {FeedsTabBar} from '../com/pager/FeedsTabBar'
-import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
+import {Pager, RenderTabBarFnProps, PagerRef} from 'view/com/pager/Pager'
 import {FeedPage} from 'view/com/feeds/FeedPage'
 import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
@@ -16,25 +16,19 @@ import {usePinnedFeedsInfos, FeedSourceInfo} from '#/state/queries/feed'
 import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
 import {emitSoftReset} from '#/state/events'
 import {useSession} from '#/state/session'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import * as persisted from '#/state/persisted'
+import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export function HomeScreen(props: Props) {
   const {data: preferences} = usePreferencesQuery()
   const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
     usePinnedFeedsInfos()
-  const {isDesktop} = useWebMediaQueries()
-  const [rawInitialFeed] = React.useState<string>(
-    () => persisted.get('lastSelectedHomeFeed') ?? 'home',
-  )
   if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
     return (
       <HomeScreenReady
         {...props}
         preferences={preferences}
         pinnedFeedInfos={pinnedFeedInfos}
-        rawInitialFeed={isDesktop ? 'home' : rawInitialFeed}
       />
     )
   } else {
@@ -49,11 +43,9 @@ export function HomeScreen(props: Props) {
 function HomeScreenReady({
   preferences,
   pinnedFeedInfos,
-  rawInitialFeed,
 }: Props & {
   preferences: UsePreferencesQueryResponse
   pinnedFeedInfos: FeedSourceInfo[]
-  rawInitialFeed: string
 }) {
   const allFeeds = React.useMemo(() => {
     const feeds: FeedDescriptor[] = []
@@ -68,12 +60,24 @@ function HomeScreenReady({
     return feeds
   }, [pinnedFeedInfos])
 
-  const [rawSelectedFeed, setSelectedFeed] =
-    React.useState<string>(rawInitialFeed)
+  const rawSelectedFeed = useSelectedFeed()
+  const setSelectedFeed = useSetSelectedFeed()
   const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed as FeedDescriptor)
   const selectedIndex = Math.max(0, maybeFoundIndex)
   const selectedFeed = allFeeds[selectedIndex]
 
+  const pagerRef = React.useRef<PagerRef>(null)
+  const lastPagerReportedIndexRef = React.useRef(selectedIndex)
+  React.useLayoutEffect(() => {
+    // Since the pager is not a controlled component, adjust it imperatively
+    // if the selected index gets out of sync with what it last reported.
+    // This is supposed to only happen on the web when you use the right nav.
+    if (selectedIndex !== lastPagerReportedIndexRef.current) {
+      lastPagerReportedIndexRef.current = selectedIndex
+      pagerRef.current?.setPage(selectedIndex)
+    }
+  }, [selectedIndex])
+
   const {hasSession} = useSession()
   const setMinimalShellMode = useSetMinimalShellMode()
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
@@ -93,7 +97,7 @@ function HomeScreenReady({
       setDrawerSwipeDisabled(index > 0)
       const feed = allFeeds[index]
       setSelectedFeed(feed)
-      persisted.write('lastSelectedHomeFeed', feed)
+      lastPagerReportedIndexRef.current = index
     },
     [setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds],
   )
@@ -147,6 +151,7 @@ function HomeScreenReady({
   return hasSession ? (
     <Pager
       key={allFeeds.join(',')}
+      ref={pagerRef}
       testID="homeScreen"
       initialPage={selectedIndex}
       onPageSelected={onPageSelected}
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index a8f5f1c66..c3b1caa35 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -1,18 +1,24 @@
 import React from 'react'
 import {View, StyleSheet} from 'react-native'
-import {useNavigationState} from '@react-navigation/native'
+import {useNavigationState, useNavigation} from '@react-navigation/native'
 import {usePalette} from 'lib/hooks/usePalette'
 import {TextLink} from 'view/com/util/Link'
 import {getCurrentRoute} from 'lib/routes/helpers'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {usePinnedFeedsInfos} from '#/state/queries/feed'
+import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
+import {FeedDescriptor} from '#/state/queries/post-feed'
+import {NavigationProp} from 'lib/routes/types'
+import {emitSoftReset} from '#/state/events'
 
 export function DesktopFeeds() {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {feeds} = usePinnedFeedsInfos()
-
+  const {feeds: pinnedFeedInfos} = usePinnedFeedsInfos()
+  const selectedFeed = useSelectedFeed()
+  const setSelectedFeed = useSetSelectedFeed()
+  const navigation = useNavigation<NavigationProp>()
   const route = useNavigationState(state => {
     if (!state) {
       return {name: 'Home'}
@@ -22,30 +28,34 @@ export function DesktopFeeds() {
 
   return (
     <View style={[styles.container, pal.view]}>
-      <FeedItem href="/" title="Following" current={route.name === 'Home'} />
-      {feeds
-        .filter(f => f.displayName !== 'Following')
-        .map(feed => {
-          try {
-            const params = route.params as Record<string, string>
-            const routeName =
-              feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList'
-            return (
-              <FeedItem
-                key={feed.uri}
-                href={feed.route.href}
-                title={feed.displayName}
-                current={
-                  route.name === routeName &&
-                  params.name === feed.route.params.name &&
-                  params.rkey === feed.route.params.rkey
-                }
-              />
-            )
-          } catch {
-            return null
-          }
-        })}
+      {pinnedFeedInfos.map(feedInfo => {
+        const uri = feedInfo.uri
+        let feed: FeedDescriptor
+        if (!uri) {
+          feed = 'home'
+        } else if (uri.includes('app.bsky.feed.generator')) {
+          feed = `feedgen|${uri}`
+        } else if (uri.includes('app.bsky.graph.list')) {
+          feed = `list|${uri}`
+        } else {
+          return null
+        }
+        return (
+          <FeedItem
+            key={feed}
+            href={'/?' + new URLSearchParams([['feed', feed]])}
+            title={feedInfo.displayName}
+            current={route.name === 'Home' && feed === selectedFeed}
+            onPress={() => {
+              setSelectedFeed(feed)
+              navigation.navigate('Home')
+              if (feed === selectedFeed) {
+                emitSoftReset()
+              }
+            }}
+          />
+        )
+      })}
       <View style={{paddingTop: 8, paddingBottom: 6}}>
         <TextLink
           type="lg"
@@ -62,10 +72,12 @@ function FeedItem({
   title,
   href,
   current,
+  onPress,
 }: {
   title: string
   href: string
   current: boolean
+  onPress: () => void
 }) {
   const pal = usePalette('default')
   return (
@@ -74,6 +86,7 @@ function FeedItem({
         type="xl"
         href={href}
         text={title}
+        onPress={onPress}
         style={[
           current ? pal.text : pal.textLight,
           {letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'},