diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/me.ts | 1 | ||||
-rw-r--r-- | src/state/models/my-follows.ts | 6 | ||||
-rw-r--r-- | src/state/models/session.ts | 10 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 68 | ||||
-rw-r--r-- | src/view/com/posts/FollowingEmptyState.tsx | 81 | ||||
-rw-r--r-- | src/view/com/util/Pager.tsx | 15 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 107 |
7 files changed, 178 insertions, 110 deletions
diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 077c65595..192e8f19f 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -33,6 +33,7 @@ export class MeModel { clear() { this.mainFeed.clear() this.notifications.clear() + this.follows.clear() this.did = '' this.handle = '' this.displayName = '' diff --git a/src/state/models/my-follows.ts b/src/state/models/my-follows.ts index 732c2fe73..bf1bf9600 100644 --- a/src/state/models/my-follows.ts +++ b/src/state/models/my-follows.ts @@ -35,6 +35,12 @@ export class MyFollowsModel { // public api // = + clear() { + this.followDidToRecordMap = {} + this.lastSync = 0 + this.myDid = undefined + } + fetchIfNeeded = bundleAsync(async () => { if ( this.myDid !== this.rootStore.me.did || diff --git a/src/state/models/session.ts b/src/state/models/session.ts index efd7a5fa8..e131b2b2c 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -154,13 +154,13 @@ export class SessionModel { /** * Sets the active session */ - setActiveSession(agent: AtpAgent, did: string) { + async setActiveSession(agent: AtpAgent, did: string) { this._log('SessionModel:setActiveSession') this.data = { service: agent.service.toString(), did, } - this.rootStore.handleSessionChange(agent) + await this.rootStore.handleSessionChange(agent) } /** @@ -304,7 +304,7 @@ export class SessionModel { return false } - this.setActiveSession(agent, account.did) + await this.setActiveSession(agent, account.did) return true } @@ -337,7 +337,7 @@ export class SessionModel { }, ) - this.setActiveSession(agent, did) + await this.setActiveSession(agent, did) this._log('SessionModel:login succeeded') } @@ -376,7 +376,7 @@ export class SessionModel { }, ) - this.setActiveSession(agent, did) + await this.setActiveSession(agent, did) this._log('SessionModel:createAccount succeeded') } diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index d23455b51..c910b70e7 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -7,23 +7,15 @@ import { StyleSheet, ViewStyle, } from 'react-native' -import {useNavigation} from '@react-navigation/native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {CenteredView, FlatList} from '../util/Views' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ViewHeader} from '../util/ViewHeader' -import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' -import {Button} from '../util/forms/Button' import {FeedModel} from 'state/models/feed-view' import {FeedSlice} from './FeedSlice' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {MagnifyingGlassIcon} from 'lib/icons' -import {NavigationProp} from 'lib/routes/types' const HEADER_ITEM = {_reactKey: '__header__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -36,6 +28,7 @@ export const Feed = observer(function Feed({ scrollElRef, onPressTryAgain, onScroll, + renderEmptyState, testID, headerOffset = 0, }: { @@ -45,14 +38,12 @@ export const Feed = observer(function Feed({ scrollElRef?: MutableRefObject<FlatList<any> | null> onPressTryAgain?: () => void onScroll?: OnScrollCb + renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number }) { - const pal = usePalette('default') - const palInverted = usePalette('inverted') const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) - const navigation = useNavigation<NavigationProp>() const data = React.useMemo(() => { let feedItems: any[] = [HEADER_ITEM] @@ -82,6 +73,7 @@ export const Feed = observer(function Feed({ } setIsRefreshing(false) }, [feed, track, setIsRefreshing]) + const onEndReached = React.useCallback(async () => { track('Feed:onEndReached') try { @@ -97,37 +89,10 @@ export const Feed = observer(function Feed({ const renderItem = React.useCallback( ({item}: {item: any}) => { if (item === EMPTY_FEED_ITEM) { - return ( - <View style={styles.emptyContainer}> - <View style={styles.emptyIconContainer}> - <MagnifyingGlassIcon - style={[styles.emptyIcon, pal.text]} - size={62} - /> - </View> - <Text type="xl-medium" style={[s.textCenter, pal.text]}> - Your feed is empty! You should follow some accounts to fix this. - </Text> - <Button - type="inverted" - style={styles.emptyBtn} - onPress={ - () => - navigation.navigate( - 'SearchTab', - ) /* TODO make sure it goes to root of the tab */ - }> - <Text type="lg-medium" style={palInverted.text}> - Find accounts - </Text> - <FontAwesomeIcon - icon="angle-right" - style={palInverted.text as FontAwesomeIconStyle} - size={14} - /> - </Button> - </View> - ) + if (renderEmptyState) { + return renderEmptyState() + } + return <View /> } else if (item === ERROR_FEED_ITEM) { return ( <ErrorMessage @@ -140,7 +105,7 @@ export const Feed = observer(function Feed({ } return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} /> }, - [feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, navigation], + [feed, onPressTryAgain, showPostFollowBtn, renderEmptyState], ) const FeedFooter = React.useCallback( @@ -187,21 +152,4 @@ export const Feed = observer(function Feed({ const styles = StyleSheet.create({ feedFooter: {paddingTop: 20}, - emptyContainer: { - paddingVertical: 40, - paddingHorizontal: 30, - }, - emptyIconContainer: { - marginBottom: 16, - }, - emptyIcon: { - marginLeft: 'auto', - marginRight: 'auto', - }, - emptyBtn: { - marginTop: 20, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, }) diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx new file mode 100644 index 000000000..acd035f21 --- /dev/null +++ b/src/view/com/posts/FollowingEmptyState.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useNavigation} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {MagnifyingGlassIcon} from 'lib/icons' +import {NavigationProp} from 'lib/routes/types' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' + +export function FollowingEmptyState() { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const navigation = useNavigation<NavigationProp>() + + const onPressFindAccounts = React.useCallback(() => { + navigation.navigate('SearchTab') + navigation.popToTop() + }, [navigation]) + + return ( + <View style={styles.emptyContainer}> + <View style={styles.emptyIconContainer}> + <MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} /> + </View> + <Text type="xl-medium" style={[s.textCenter, pal.text]}> + Your following feed is empty! Find some accounts to follow to fix this. + </Text> + <Button + type="inverted" + style={styles.emptyBtn} + onPress={onPressFindAccounts}> + <Text type="lg-medium" style={palInverted.text}> + Find accounts to follow + </Text> + <FontAwesomeIcon + icon="angle-right" + style={palInverted.text as FontAwesomeIconStyle} + size={14} + /> + </Button> + </View> + ) +} +const styles = StyleSheet.create({ + emptyContainer: { + // flex: 1, + height: '100%', + paddingVertical: 40, + paddingHorizontal: 30, + }, + emptyIconContainer: { + marginBottom: 16, + }, + emptyIcon: { + marginLeft: 'auto', + marginRight: 'auto', + }, + emptyBtn: { + marginVertical: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 18, + paddingHorizontal: 24, + borderRadius: 30, + }, + + feedsTip: { + position: 'absolute', + left: 22, + }, + feedsTipArrow: { + marginLeft: 32, + marginTop: 8, + }, +}) diff --git a/src/view/com/util/Pager.tsx b/src/view/com/util/Pager.tsx index c4f17ce65..d71cb7f7f 100644 --- a/src/view/com/util/Pager.tsx +++ b/src/view/com/util/Pager.tsx @@ -1,21 +1,30 @@ import React from 'react' import {Animated, View} from 'react-native' import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' -import {TabBarProps} from './TabBar' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {s} from 'lib/styles' export type PageSelectedEvent = PagerViewOnPageSelectedEvent const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) +export interface RenderTabBarFnProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} +export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element + interface Props { tabBarPosition?: 'top' | 'bottom' - renderTabBar: (props: TabBarProps) => JSX.Element + initialPage?: number + renderTabBar: RenderTabBarFn onPageSelected?: (e: PageSelectedEvent) => void } export const Pager = ({ children, tabBarPosition = 'top', + initialPage = 0, renderTabBar, onPageSelected, }: React.PropsWithChildren<Props>) => { @@ -51,7 +60,7 @@ export const Pager = ({ <AnimatedPagerView ref={pagerView} style={s.h100pct} - initialPage={0} + initialPage={initialPage} onPageSelected={onPageSelectedInner} onPageScroll={Animated.event( [ diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 1c3ee091a..822174446 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -13,9 +13,14 @@ import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {FeedModel} from 'state/models/feed-view' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {Feed} from '../com/posts/Feed' +import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' -import {TabBar, TabBarProps} from 'view/com/util/TabBar' -import {Pager, PageSelectedEvent} from 'view/com/util/Pager' +import {TabBar} from 'view/com/util/TabBar' +import { + Pager, + PageSelectedEvent, + RenderTabBarFnProps, +} from 'view/com/util/Pager' import {FAB} from '../com/util/FAB' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' @@ -63,69 +68,86 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { }, [store]) const renderTabBar = React.useCallback( - (props: TabBarProps) => { + (props: RenderTabBarFnProps) => { return <FloatingTabBar {...props} onPressSelected={onPressSelected} /> }, [onPressSelected], ) + const renderFollowingEmptyState = React.useCallback(() => { + return <FollowingEmptyState /> + }, []) + + const initialPage = store.me.follows.isEmpty ? 1 : 0 return ( <Pager onPageSelected={onPageSelected} renderTabBar={renderTabBar} - tabBarPosition="bottom"> + tabBarPosition="bottom" + initialPage={initialPage}> <FeedPage key="1" isPageFocused={selectedPage === 0} feed={store.me.mainFeed} + renderEmptyState={renderFollowingEmptyState} /> <FeedPage key="2" isPageFocused={selectedPage === 1} feed={algoFeed} /> </Pager> ) }) -const FloatingTabBar = observer((props: TabBarProps) => { - const store = useStores() - const safeAreaInsets = useSafeAreaInsets() - const pal = usePalette('default') - const interp = useAnimatedValue(0) - - const pad = React.useMemo( - () => ({ - paddingBottom: clamp(safeAreaInsets.bottom, 15, 20), - }), - [safeAreaInsets], - ) +const FloatingTabBar = observer( + (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + const store = useStores() + const safeAreaInsets = useSafeAreaInsets() + const pal = usePalette('default') + const interp = useAnimatedValue(0) - React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 0 : 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) - const transform = { - transform: [ - {translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)}, - ], - } + const pad = React.useMemo( + () => ({ + paddingBottom: clamp(safeAreaInsets.bottom, 15, 20), + }), + [safeAreaInsets], + ) - return ( - <Animated.View - style={[pal.view, pal.border, styles.tabBar, pad, transform]}> - <TabBar - {...props} - items={['Following', "What's hot"]} - indicatorPosition="top" - indicatorColor={pal.colors.link} - /> - </Animated.View> - ) -}) + React.useEffect(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 0 : 1, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + }, [interp, store.shell.minimalShellMode]) + const transform = { + transform: [ + {translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)}, + ], + } + + return ( + <Animated.View + style={[pal.view, pal.border, styles.tabBar, pad, transform]}> + <TabBar + {...props} + items={['Following', "What's hot"]} + indicatorPosition="top" + indicatorColor={pal.colors.link} + /> + </Animated.View> + ) + }, +) const FeedPage = observer( - ({isPageFocused, feed}: {feed: FeedModel; isPageFocused: boolean}) => { + ({ + isPageFocused, + feed, + renderEmptyState, + }: { + feed: FeedModel + isPageFocused: boolean + renderEmptyState?: () => JSX.Element + }) => { const store = useStores() const onMainScroll = useOnMainScroll(store) const {screen, track} = useAnalytics() @@ -213,6 +235,7 @@ const FeedPage = observer( showPostFollowBtn onPressTryAgain={onPressTryAgain} onScroll={onMainScroll} + renderEmptyState={renderEmptyState} /> {feed.hasNewLatest && !feed.isRefreshing && ( <LoadLatestBtn onPress={onPressLoadLatest} /> |