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
zio/stable
dan 2024-02-08 20:37:08 +00:00 committed by GitHub
parent 80c482b026
commit 06f81d6948
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 148 additions and 63 deletions

View File

@ -32,6 +32,7 @@ import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences' import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import I18nProvider from './locale/i18nProvider' import I18nProvider from './locale/i18nProvider'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
@ -72,17 +73,19 @@ function InnerApp() {
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<LoggedOutViewProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<ThemeProvider theme={theme}> <UnreadNotifsProvider>
{/* All components should be within this provider */} <ThemeProvider theme={theme}>
<RootSiblingParent> {/* All components should be within this provider */}
<GestureHandlerRootView style={s.h100pct}> <RootSiblingParent>
<TestCtrls /> <GestureHandlerRootView style={s.h100pct}>
<Shell /> <TestCtrls />
</GestureHandlerRootView> <Shell />
</RootSiblingParent> </GestureHandlerRootView>
</ThemeProvider> </RootSiblingParent>
</UnreadNotifsProvider> </ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider> </LoggedOutViewProvider>
</React.Fragment> </React.Fragment>
</Splash> </Splash>

View File

@ -22,6 +22,7 @@ import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences' import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import I18nProvider from './locale/i18nProvider' import I18nProvider from './locale/i18nProvider'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
@ -52,17 +53,19 @@ function InnerApp() {
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<LoggedOutViewProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<ThemeProvider theme={theme}> <UnreadNotifsProvider>
{/* All components should be within this provider */} <ThemeProvider theme={theme}>
<RootSiblingParent> {/* All components should be within this provider */}
<SafeAreaProvider> <RootSiblingParent>
<Shell /> <SafeAreaProvider>
</SafeAreaProvider> <Shell />
</RootSiblingParent> </SafeAreaProvider>
<ToastContainer /> </RootSiblingParent>
</ThemeProvider> <ToastContainer />
</UnreadNotifsProvider> </ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider> </LoggedOutViewProvider>
</React.Fragment> </React.Fragment>
</Alf> </Alf>

View File

@ -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)
}

View File

@ -31,7 +31,7 @@ export const Pager = React.forwardRef(function PagerImpl(
const anchorRef = React.useRef(null) const anchorRef = React.useRef(null)
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
setPage: (index: number) => setSelectedPage(index), setPage: (index: number) => onTabBarSelect(index),
})) }))
const onTabBarSelect = React.useCallback( const onTabBarSelect = React.useCallback(

View File

@ -7,7 +7,7 @@ import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FeedsTabBar} from '../com/pager/FeedsTabBar' 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 {FeedPage} from 'view/com/feeds/FeedPage'
import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' 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 {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {emitSoftReset} from '#/state/events' import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import * as persisted from '#/state/persisted'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) { export function HomeScreen(props: Props) {
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} = const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos() usePinnedFeedsInfos()
const {isDesktop} = useWebMediaQueries()
const [rawInitialFeed] = React.useState<string>(
() => persisted.get('lastSelectedHomeFeed') ?? 'home',
)
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) { if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
return ( return (
<HomeScreenReady <HomeScreenReady
{...props} {...props}
preferences={preferences} preferences={preferences}
pinnedFeedInfos={pinnedFeedInfos} pinnedFeedInfos={pinnedFeedInfos}
rawInitialFeed={isDesktop ? 'home' : rawInitialFeed}
/> />
) )
} else { } else {
@ -49,11 +43,9 @@ export function HomeScreen(props: Props) {
function HomeScreenReady({ function HomeScreenReady({
preferences, preferences,
pinnedFeedInfos, pinnedFeedInfos,
rawInitialFeed,
}: Props & { }: Props & {
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
pinnedFeedInfos: FeedSourceInfo[] pinnedFeedInfos: FeedSourceInfo[]
rawInitialFeed: string
}) { }) {
const allFeeds = React.useMemo(() => { const allFeeds = React.useMemo(() => {
const feeds: FeedDescriptor[] = [] const feeds: FeedDescriptor[] = []
@ -68,12 +60,24 @@ function HomeScreenReady({
return feeds return feeds
}, [pinnedFeedInfos]) }, [pinnedFeedInfos])
const [rawSelectedFeed, setSelectedFeed] = const rawSelectedFeed = useSelectedFeed()
React.useState<string>(rawInitialFeed) const setSelectedFeed = useSetSelectedFeed()
const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed as FeedDescriptor) const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed as FeedDescriptor)
const selectedIndex = Math.max(0, maybeFoundIndex) const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex] 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 {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
@ -93,7 +97,7 @@ function HomeScreenReady({
setDrawerSwipeDisabled(index > 0) setDrawerSwipeDisabled(index > 0)
const feed = allFeeds[index] const feed = allFeeds[index]
setSelectedFeed(feed) setSelectedFeed(feed)
persisted.write('lastSelectedHomeFeed', feed) lastPagerReportedIndexRef.current = index
}, },
[setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds], [setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds],
) )
@ -147,6 +151,7 @@ function HomeScreenReady({
return hasSession ? ( return hasSession ? (
<Pager <Pager
key={allFeeds.join(',')} key={allFeeds.join(',')}
ref={pagerRef}
testID="homeScreen" testID="homeScreen"
initialPage={selectedIndex} initialPage={selectedIndex}
onPageSelected={onPageSelected} onPageSelected={onPageSelected}

View File

@ -1,18 +1,24 @@
import React from 'react' import React from 'react'
import {View, StyleSheet} from 'react-native' 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 {usePalette} from 'lib/hooks/usePalette'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {getCurrentRoute} from 'lib/routes/helpers' import {getCurrentRoute} from 'lib/routes/helpers'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {usePinnedFeedsInfos} from '#/state/queries/feed' 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() { export function DesktopFeeds() {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {feeds} = usePinnedFeedsInfos() const {feeds: pinnedFeedInfos} = usePinnedFeedsInfos()
const selectedFeed = useSelectedFeed()
const setSelectedFeed = useSetSelectedFeed()
const navigation = useNavigation<NavigationProp>()
const route = useNavigationState(state => { const route = useNavigationState(state => {
if (!state) { if (!state) {
return {name: 'Home'} return {name: 'Home'}
@ -22,30 +28,34 @@ export function DesktopFeeds() {
return ( return (
<View style={[styles.container, pal.view]}> <View style={[styles.container, pal.view]}>
<FeedItem href="/" title="Following" current={route.name === 'Home'} /> {pinnedFeedInfos.map(feedInfo => {
{feeds const uri = feedInfo.uri
.filter(f => f.displayName !== 'Following') let feed: FeedDescriptor
.map(feed => { if (!uri) {
try { feed = 'home'
const params = route.params as Record<string, string> } else if (uri.includes('app.bsky.feed.generator')) {
const routeName = feed = `feedgen|${uri}`
feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList' } else if (uri.includes('app.bsky.graph.list')) {
return ( feed = `list|${uri}`
<FeedItem } else {
key={feed.uri} return null
href={feed.route.href} }
title={feed.displayName} return (
current={ <FeedItem
route.name === routeName && key={feed}
params.name === feed.route.params.name && href={'/?' + new URLSearchParams([['feed', feed]])}
params.rkey === feed.route.params.rkey title={feedInfo.displayName}
} current={route.name === 'Home' && feed === selectedFeed}
/> onPress={() => {
) setSelectedFeed(feed)
} catch { navigation.navigate('Home')
return null if (feed === selectedFeed) {
} emitSoftReset()
})} }
}}
/>
)
})}
<View style={{paddingTop: 8, paddingBottom: 6}}> <View style={{paddingTop: 8, paddingBottom: 6}}>
<TextLink <TextLink
type="lg" type="lg"
@ -62,10 +72,12 @@ function FeedItem({
title, title,
href, href,
current, current,
onPress,
}: { }: {
title: string title: string
href: string href: string
current: boolean current: boolean
onPress: () => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
@ -74,6 +86,7 @@ function FeedItem({
type="xl" type="xl"
href={href} href={href}
text={title} text={title}
onPress={onPress}
style={[ style={[
current ? pal.text : pal.textLight, current ? pal.text : pal.textLight,
{letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'}, {letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'},