diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-03-16 21:47:11 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2023-03-16 21:47:11 -0500 |
commit | 71209bb3aca1104773ee9fab20ec9f9ab4fc3ad2 (patch) | |
tree | fc2de0e2a1ed13b972628cca2e7b6fa7c38ecfe8 | |
parent | ad9da82612a33a796bcb2c679dbff357f4829dc8 (diff) | |
download | voidsky-71209bb3aca1104773ee9fab20ec9f9ab4fc3ad2.tar.zst |
Implement popular & following views on home screen
-rw-r--r-- | src/state/models/feed-view.ts | 12 | ||||
-rw-r--r-- | src/view/com/profile/FollowButton.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/Pager.tsx | 27 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 8 | ||||
-rw-r--r-- | src/view/com/util/TabBar.tsx | 25 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 360 |
6 files changed, 272 insertions, 162 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 42b753b24..81760132e 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -257,7 +257,7 @@ export class FeedModel { constructor( public rootStore: RootStoreModel, - public feedType: 'home' | 'author' | 'suggested', + public feedType: 'home' | 'author' | 'suggested' | 'goodstuff', params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams, ) { makeAutoObservable( @@ -634,6 +634,16 @@ export class FeedModel { return this.rootStore.api.app.bsky.feed.getTimeline( params as GetTimeline.QueryParams, ) + } else if (this.feedType === 'goodstuff') { + const res = await this.rootStore.api.app.bsky.feed.getAuthorFeed({ + ...params, + author: 'jay.bsky.social', + } as GetAuthorFeed.QueryParams) + res.data.feed = mergePosts([res], {repostsOnly: true}) + res.data.feed.forEach(item => { + delete item.reason + }) + return res } else { return this.rootStore.api.app.bsky.feed.getAuthorFeed( params as GetAuthorFeed.QueryParams, diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx index f24c3d0c9..7a194cee9 100644 --- a/src/view/com/profile/FollowButton.tsx +++ b/src/view/com/profile/FollowButton.tsx @@ -42,7 +42,7 @@ const FollowButton = observer( return ( <Button - type={isFollowing ? 'default' : 'primary'} + type={isFollowing ? 'default' : 'inverted'} onPress={onToggleFollowInner} label={isFollowing ? 'Unfollow' : 'Follow'} /> diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx index 1a3ff642c..9ce5006cd 100644 --- a/src/view/com/util/Pager.tsx +++ b/src/view/com/util/Pager.tsx @@ -2,16 +2,25 @@ import React from 'react' import {Animated, StyleSheet, View} from 'react-native' import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' -import {TabBar} from './TabBar' +import {s} from 'lib/styles' export type PageSelectedEvent = PagerViewOnPageSelectedEvent const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) +export interface TabBarProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} + interface Props { + renderTabBar: (props: TabBarProps) => JSX.Element onPageSelected?: (e: PageSelectedEvent) => void } export const Pager = ({ children, + renderTabBar, onPageSelected, }: React.PropsWithChildren<Props>) => { const [selectedPage, setSelectedPage] = React.useState(0) @@ -36,16 +45,10 @@ export const Pager = ({ return ( <View> - <TabBar - position={position} - offset={offset} - items={['One', 'Two', 'Three']} - selectedPage={selectedPage} - onSelect={onTabBarSelect} - /> + {renderTabBar({selectedPage, position, offset, onSelect: onTabBarSelect})} <AnimatedPagerView ref={pagerView} - style={{height: '100%'}} + style={s.h100pct} initialPage={0} onPageSelected={onPageSelectedInner} onPageScroll={Animated.event( @@ -64,9 +67,3 @@ export const Pager = ({ </View> ) } - -const styles = StyleSheet.create({ - tabBar: { - flexDirection: 'row', - }, -}) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 0bb402100..3f9e6935f 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -44,7 +44,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { // two-liner with follow button return ( <View style={styles.metaTwoLine}> - <View> + <View style={styles.metaTwoLineLeft}> <View style={styles.metaTwoLineTop}> <DesktopWebTextLink type="lg-bold" @@ -69,6 +69,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { type="md" style={[styles.metaItem, pal.textLight]} lineHeight={1.2} + numberOfLines={1} text={`@${handle}`} href={`/profile/${opts.authorHandle}`} /> @@ -134,8 +135,13 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', + width: '100%', paddingBottom: 2, }, + metaTwoLineLeft: { + flex: 1, + paddingRight: 40, + }, metaTwoLineTop: { flexDirection: 'row', alignItems: 'baseline', diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx index 3a823e42c..d9f48577c 100644 --- a/src/view/com/util/TabBar.tsx +++ b/src/view/com/util/TabBar.tsx @@ -37,7 +37,7 @@ export function TabBar({ const panX = Animated.add(position, offset) const underlineStyle = { - backgroundColor: pal.colors.text, + backgroundColor: pal.colors.link, left: panX.interpolate({ inputRange: items.map((_item, i) => i), outputRange: itemLayouts.map(l => l.x), @@ -79,11 +79,8 @@ export function TabBar({ <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}> <View style={styles.item} ref={itemRefs[i]}> <Text - style={ - selected - ? [styles.labelSelected, pal.text] - : [styles.label, pal.textLight] - }> + type="xl-medium" + style={selected ? pal.text : pal.textLight}> {item} </Text> </View> @@ -100,20 +97,14 @@ const styles = StyleSheet.create({ paddingHorizontal: 14, }, item: { - paddingTop: 8, - paddingBottom: 12, - marginRight: 14, - paddingHorizontal: 10, - }, - label: { - fontWeight: '600', - }, - labelSelected: { - fontWeight: '600', + paddingTop: 6, + paddingBottom: 14, + marginRight: 24, }, underline: { position: 'absolute', - height: 4, + height: 3, bottom: 0, + borderRadius: 4, }, }) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 6c708e2fd..fa200e931 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,15 +1,23 @@ import React from 'react' -import {FlatList, StyleSheet, View, useWindowDimensions} from 'react-native' +import { + FlatList, + StyleSheet, + TouchableOpacity, + View, + useWindowDimensions, +} from 'react-native' import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' +import {FeedModel} from 'state/models/feed-view' import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/posts/Feed' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {WelcomeBanner} from '../com/util/WelcomeBanner' import {UserAvatar} from 'view/com/util/UserAvatar' +import {TabBar} from 'view/com/util/TabBar' +import {Pager, PageSelectedEvent, TabBarProps} from 'view/com/util/Pager' import {FAB} from '../com/util/FAB' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' @@ -18,157 +26,255 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics' import {ComposeIcon2} from 'lib/icons' -import {Pager, PageSelectedEvent} from 'view/com/util/Pager' -import {Text} from 'view/com/util/text/Text' - -const HEADER_HEIGHT = 42 +const TAB_BAR_HEIGHT = 82 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> export const HomeScreen = withAuthRequired((_opts: Props) => { const store = useStores() + const pal = usePalette('default') + const [selectedPage, setSelectedPage] = React.useState(0) + + useFocusEffect( + React.useCallback(() => { + store.shell.setIsDrawerSwipeDisabled(selectedPage > 0) + return () => { + store.shell.setIsDrawerSwipeDisabled(false) + } + }, [store, selectedPage]), + ) + const onPageSelected = React.useCallback( (e: PageSelectedEvent) => { + setSelectedPage(e.nativeEvent.position) store.shell.setIsDrawerSwipeDisabled(e.nativeEvent.position > 0) }, [store], ) + const onPressAvi = React.useCallback(() => { + store.shell.openDrawer() + }, [store]) + + const renderTabBar = React.useCallback( + (props: TabBarProps) => { + return ( + <View style={[pal.view, styles.tabBar]}> + <TouchableOpacity style={styles.tabBarAvi} onPress={onPressAvi}> + <UserAvatar avatar={store.me.avatar} size={32} /> + </TouchableOpacity> + <TabBar items={['Popular', 'Following']} {...props} /> + </View> + ) + }, + [store.me.avatar, pal, onPressAvi], + ) + + return ( + <Pager onPageSelected={onPageSelected} renderTabBar={renderTabBar}> + <AlgoView key="1" /> + <View key="2"> + <FollowingView /> + </View> + </Pager> + ) +}) + +const AlgoView = observer(() => { + const store = useStores() + const onMainScroll = useOnMainScroll(store) + const {screen, track} = useAnalytics() + const scrollElRef = React.useRef<FlatList>(null) + const {appState} = useAppState({ + onForeground: () => doPoll(true), + }) + const isFocused = useIsFocused() + const winDim = useWindowDimensions() + const containerStyle = React.useMemo( + () => ({height: winDim.height - TAB_BAR_HEIGHT}), + [winDim], + ) + const algoFeed = React.useMemo(() => { + const feed = new FeedModel(store, 'goodstuff', {}) + feed.setup() + return feed + }, [store]) + + const doPoll = React.useCallback( + (knownActive = false) => { + if ((!knownActive && appState !== 'active') || !isFocused) { + return + } + if (algoFeed.isLoading) { + return + } + store.log.debug('HomeScreen: Polling for new posts') + algoFeed.checkForLatest() + }, + [appState, isFocused, store, algoFeed], + ) + + const scrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0}) + }, [scrollElRef]) + useFocusEffect( React.useCallback(() => { + const softResetSub = store.onScreenSoftReset(scrollToTop) + const feedCleanup = algoFeed.registerListeners() + const pollInterval = setInterval(doPoll, 15e3) + + screen('Feed') + store.log.debug('HomeScreen: Updating feed') + if (algoFeed.hasContent) { + algoFeed.update() + } + return () => { - store.shell.setIsDrawerSwipeDisabled(false) + clearInterval(pollInterval) + softResetSub.remove() + feedCleanup() } - }, [store]), + }, [store, doPoll, scrollToTop, screen, algoFeed]), ) + const onPressCompose = React.useCallback(() => { + track('HomeScreen:PressCompose') + store.shell.openComposer({}) + }, [store, track]) + + const onPressTryAgain = React.useCallback(() => { + algoFeed.refresh() + }, [algoFeed]) + + const onPressLoadLatest = React.useCallback(() => { + algoFeed.refresh() + scrollToTop() + }, [algoFeed, scrollToTop]) + return ( - <Pager onPageSelected={onPageSelected}> - <View key="1"> - <MyPage>First page</MyPage> - </View> - <View key="2"> - <MyPage>Second page</MyPage> - </View> - <View key="3"> - <MyPage>Third page</MyPage> - </View> - </Pager> + <View style={containerStyle}> + {store.shell.isOnboarding && <WelcomeBanner />} + <Feed + testID="homeFeed" + key="default" + feed={algoFeed} + scrollElRef={scrollElRef} + style={s.hContentRegion} + showPostFollowBtn + onPressTryAgain={onPressTryAgain} + onScroll={onMainScroll} + /> + {algoFeed.hasNewLatest && !algoFeed.isRefreshing && ( + <LoadLatestBtn onPress={onPressLoadLatest} /> + )} + <FAB + testID="composeFAB" + onPress={onPressCompose} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + /> + </View> ) }) -function MyPage({children}) { + +const FollowingView = observer(() => { + const store = useStores() + const onMainScroll = useOnMainScroll(store) + const {screen, track} = useAnalytics() + const scrollElRef = React.useRef<FlatList>(null) + const {appState} = useAppState({ + onForeground: () => doPoll(true), + }) + const isFocused = useIsFocused() + const winDim = useWindowDimensions() + const containerStyle = React.useMemo( + () => ({height: winDim.height - TAB_BAR_HEIGHT}), + [winDim], + ) + + const doPoll = React.useCallback( + (knownActive = false) => { + if ((!knownActive && appState !== 'active') || !isFocused) { + return + } + if (store.me.mainFeed.isLoading) { + return + } + store.log.debug('HomeScreen: Polling for new posts') + store.me.mainFeed.checkForLatest() + }, + [appState, isFocused, store], + ) + + const scrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0}) + }, [scrollElRef]) + + useFocusEffect( + React.useCallback(() => { + const softResetSub = store.onScreenSoftReset(scrollToTop) + const feedCleanup = store.me.mainFeed.registerListeners() + const pollInterval = setInterval(doPoll, 15e3) + + screen('Feed') + store.log.debug('HomeScreen: Updating feed') + if (store.me.mainFeed.hasContent) { + store.me.mainFeed.update() + } + + return () => { + clearInterval(pollInterval) + softResetSub.remove() + feedCleanup() + } + }, [store, doPoll, scrollToTop, screen]), + ) + + const onPressCompose = React.useCallback(() => { + track('HomeScreen:PressCompose') + store.shell.openComposer({}) + }, [store, track]) + + const onPressTryAgain = React.useCallback(() => { + store.me.mainFeed.refresh() + }, [store]) + + const onPressLoadLatest = React.useCallback(() => { + store.me.mainFeed.refresh() + scrollToTop() + }, [store, scrollToTop]) + return ( - <View - style={{ - flex: 1, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 1, - backgroundColor: 'white', - }}> - <Text>{children}</Text> + <View style={containerStyle}> + {store.shell.isOnboarding && <WelcomeBanner />} + <Feed + testID="homeFeed" + key="default" + feed={store.me.mainFeed} + scrollElRef={scrollElRef} + style={s.hContentRegion} + showPostFollowBtn + onPressTryAgain={onPressTryAgain} + onScroll={onMainScroll} + /> + {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( + <LoadLatestBtn onPress={onPressLoadLatest} /> + )} + <FAB + testID="composeFAB" + onPress={onPressCompose} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + /> </View> ) -} +}) const styles = StyleSheet.create({ tabBar: { flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 18, + }, + tabBarAvi: { + marginRight: 16, }, }) -/* -type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> -export const HomeScreen = withAuthRequired( - observer(function Home(_opts: Props) { - const store = useStores() - const onMainScroll = useOnMainScroll(store) - const {screen, track} = useAnalytics() - const scrollElRef = React.useRef<FlatList>(null) - const {appState} = useAppState({ - onForeground: () => doPoll(true), - }) - const isFocused = useIsFocused() - - const doPoll = React.useCallback( - (knownActive = false) => { - if ((!knownActive && appState !== 'active') || !isFocused) { - return - } - if (store.me.mainFeed.isLoading) { - return - } - store.log.debug('HomeScreen: Polling for new posts') - store.me.mainFeed.checkForLatest() - }, - [appState, isFocused, store], - ) - - const scrollToTop = React.useCallback(() => { - // NOTE: the feed is offset by the height of the collapsing header, - // so we scroll to the negative of that height -prf - scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT}) - }, [scrollElRef]) - - useFocusEffect( - React.useCallback(() => { - const softResetSub = store.onScreenSoftReset(scrollToTop) - const feedCleanup = store.me.mainFeed.registerListeners() - const pollInterval = setInterval(doPoll, 15e3) - - screen('Feed') - store.log.debug('HomeScreen: Updating feed') - if (store.me.mainFeed.hasContent) { - store.me.mainFeed.update() - } - - return () => { - clearInterval(pollInterval) - softResetSub.remove() - feedCleanup() - } - }, [store, doPoll, scrollToTop, screen]), - ) - - const onPressCompose = React.useCallback(() => { - track('HomeScreen:PressCompose') - store.shell.openComposer({}) - }, [store, track]) - - const onPressTryAgain = React.useCallback(() => { - store.me.mainFeed.refresh() - }, [store]) - - const onPressLoadLatest = React.useCallback(() => { - store.me.mainFeed.refresh() - scrollToTop() - }, [store, scrollToTop]) - - return ( - <View style={s.hContentRegion}> - {store.shell.isOnboarding && <WelcomeBanner />} - <Feed - testID="homeFeed" - key="default" - feed={store.me.mainFeed} - scrollElRef={scrollElRef} - style={s.hContentRegion} - showPostFollowBtn - onPressTryAgain={onPressTryAgain} - onScroll={onMainScroll} - headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT} - /> - {!store.shell.isOnboarding && ( - <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll /> - )} - {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( - <LoadLatestBtn onPress={onPressLoadLatest} /> - )} - <FAB - testID="composeFAB" - onPress={onPressCompose} - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} - /> - </View> - ) - }), -) -*/ |