diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 8 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/MultiFeed.tsx | 230 | ||||
-rw-r--r-- | src/view/screens/Feeds.tsx | 125 | ||||
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 6 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 30 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 30 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 2 |
9 files changed, 412 insertions, 23 deletions
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 574265eb7..0d71b2b98 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -8,7 +8,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {Link} from '../util/Link' import {Text} from '../util/text/Text' -import {SatelliteDishIcon} from 'lib/icons' +import {CogIcon} from 'lib/icons' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {s} from 'lib/styles' @@ -69,11 +69,7 @@ export const FeedsTabBar = observer( accessibilityRole="button" accessibilityLabel="Edit Saved Feeds" accessibilityHint="Opens screen to edit Saved Feeds"> - <SatelliteDishIcon - size={20} - strokeWidth={2} - style={pal.textLight} - /> + <CogIcon size={21} strokeWidth={2} style={pal.textLight} /> </Link> </View> </View> diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index bbba74c87..b0a02ea22 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -131,7 +131,7 @@ const styles = isDesktopWeb backgroundColor: 'transparent', }, contentContainer: { - columnGap: 16, + columnGap: 20, marginLeft: 18, paddingRight: 28, backgroundColor: 'transparent', diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index b90213472..8206ca509 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -33,7 +33,6 @@ export const Feed = observer(function Feed({ onPressTryAgain, onScroll, scrollEventThrottle, - onMomentumScrollEnd, renderEmptyState, testID, headerOffset = 0, @@ -186,7 +185,6 @@ export const Feed = observer(function Feed({ style={{paddingTop: headerOffset}} onScroll={onScroll} scrollEventThrottle={scrollEventThrottle} - onMomentumScrollEnd={onMomentumScrollEnd} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} onEndReachedThreshold={0.6} diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx new file mode 100644 index 000000000..4911c9e2c --- /dev/null +++ b/src/view/com/posts/MultiFeed.tsx @@ -0,0 +1,230 @@ +import React, {MutableRefObject} from 'react' +import {observer} from 'mobx-react-lite' +import { + ActivityIndicator, + RefreshControl, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {FlatList} from '../util/Views' +import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed' +import {FeedSlice} from './FeedSlice' +import {Text} from '../util/text/Text' +import {Link} from '../util/Link' +import {UserAvatar} from '../util/UserAvatar' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useAnalytics} from 'lib/analytics' +import {usePalette} from 'lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' + +export const MultiFeed = observer(function Feed({ + multifeed, + style, + showPostFollowBtn, + scrollElRef, + onScroll, + scrollEventThrottle, + testID, + headerOffset = 0, + extraData, +}: { + multifeed: PostsMultiFeedModel + style?: StyleProp<ViewStyle> + showPostFollowBtn?: boolean + scrollElRef?: MutableRefObject<FlatList<any> | null> + onPressTryAgain?: () => void + onScroll?: OnScrollCb + scrollEventThrottle?: number + renderEmptyState?: () => JSX.Element + testID?: string + headerOffset?: number + extraData?: any +}) { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const theme = useTheme() + const {track} = useAnalytics() + const [isRefreshing, setIsRefreshing] = React.useState(false) + + // events + // = + + const onRefresh = React.useCallback(async () => { + track('MultiFeed:onRefresh') + setIsRefreshing(true) + try { + await multifeed.refresh() + } catch (err) { + multifeed.rootStore.log.error('Failed to refresh posts feed', err) + } + setIsRefreshing(false) + }, [multifeed, track, setIsRefreshing]) + + const onEndReached = React.useCallback(async () => { + track('MultiFeed:onEndReached') + try { + await multifeed.loadMore() + } catch (err) { + multifeed.rootStore.log.error('Failed to load more posts', err) + } + }, [multifeed, track]) + + // rendering + // = + + const renderItem = React.useCallback( + ({item}: {item: MultiFeedItem}) => { + if (item.type === 'header') { + return <View style={[styles.header, pal.border]} /> + } else if (item.type === 'feed-header') { + return ( + <View style={styles.feedHeader}> + <UserAvatar type="algo" avatar={item.avatar} size={28} /> + <Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}> + {item.title} + </Text> + </View> + ) + } else if (item.type === 'feed-slice') { + return ( + <FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} /> + ) + } else if (item.type === 'feed-loading') { + return <PostFeedLoadingPlaceholder /> + } else if (item.type === 'feed-error') { + return <ErrorMessage message={item.error} /> + } else if (item.type === 'feed-footer') { + return ( + <Link + href={item.uri} + style={[styles.feedFooter, pal.border, pal.view]}> + <Text type="lg" style={pal.link}> + See more from {item.title} + </Text> + <FontAwesomeIcon + icon="angle-right" + size={18} + color={pal.colors.link} + /> + </Link> + ) + } else if (item.type === 'footer') { + return ( + <Link + style={[styles.footerLink, palInverted.view]} + href="/search/feeds"> + <FontAwesomeIcon + icon="search" + size={18} + color={palInverted.colors.text} + /> + <Text type="lg-medium" style={palInverted.text}> + Discover new feeds + </Text> + </Link> + ) + } + return null + }, + [showPostFollowBtn, pal, palInverted], + ) + + const FeedFooter = React.useCallback( + () => + multifeed.isLoading && !isRefreshing ? ( + <View style={styles.loadMore}> + <ActivityIndicator color={pal.colors.text} /> + </View> + ) : ( + <View /> + ), + [multifeed.isLoading, isRefreshing, pal], + ) + + return ( + <View testID={testID} style={style}> + {multifeed.items.length > 0 && ( + <FlatList + testID={testID ? `${testID}-flatlist` : undefined} + ref={scrollElRef} + data={multifeed.items} + keyExtractor={item => item._reactKey} + renderItem={renderItem} + ListFooterComponent={FeedFooter} + refreshControl={ + <RefreshControl + refreshing={isRefreshing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + progressViewOffset={headerOffset} + /> + } + contentContainerStyle={s.contentContainer} + style={[{paddingTop: headerOffset}, pal.viewLight, styles.container]} + onScroll={onScroll} + scrollEventThrottle={scrollEventThrottle} + indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} + onEndReached={onEndReached} + onEndReachedThreshold={0.6} + removeClippedSubviews={true} + contentOffset={{x: 0, y: headerOffset * -1}} + extraData={extraData} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + )} + </View> + ) +}) + +const styles = StyleSheet.create({ + container: { + height: '100%', + }, + header: { + borderTopWidth: 1, + marginBottom: 4, + }, + feedHeader: { + flexDirection: 'row', + gap: 8, + alignItems: 'center', + paddingHorizontal: 16, + paddingBottom: 8, + marginTop: 12, + }, + feedHeaderTitle: { + fontWeight: 'bold', + }, + feedFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 16, + marginBottom: 12, + borderTopWidth: 1, + borderBottomWidth: 1, + }, + footerLink: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 8, + paddingHorizontal: 14, + paddingVertical: 12, + marginHorizontal: 8, + marginBottom: 8, + gap: 8, + }, + loadMore: { + paddingTop: 10, + }, +}) diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx new file mode 100644 index 000000000..5d5ed6c16 --- /dev/null +++ b/src/view/screens/Feeds.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import isEqual from 'lodash.isequal' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {FlatList} from 'view/com/util/Views' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {FAB} from 'view/com/util/fab/FAB' +import {Link} from 'view/com/util/Link' +import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' +import {observer} from 'mobx-react-lite' +import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed' +import {MultiFeed} from 'view/com/posts/MultiFeed' +import {isDesktopWeb} from 'platform/detection' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {ComposeIcon2, CogIcon} from 'lib/icons' +import {s} from 'lib/styles' + +const HEADER_OFFSET = isDesktopWeb ? 0 : 40 + +type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> +export const FeedsScreen = withAuthRequired( + observer<Props>(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const flatListRef = React.useRef<FlatList>(null) + const multifeed = React.useMemo<PostsMultiFeedModel>( + () => new PostsMultiFeedModel(store), + [store], + ) + const [onMainScroll, isScrolledDown, resetMainScroll] = + useOnMainScroll(store) + + const onSoftReset = React.useCallback(() => { + flatListRef.current?.scrollToOffset({offset: 0}) + resetMainScroll() + }, [flatListRef, resetMainScroll]) + + useFocusEffect( + React.useCallback(() => { + const softResetSub = store.onScreenSoftReset(onSoftReset) + const multifeedCleanup = multifeed.registerListeners() + const cleanup = () => { + softResetSub.remove() + multifeedCleanup() + } + + store.shell.setMinimalShellMode(false) + return cleanup + }, [store, multifeed, onSoftReset]), + ) + + React.useEffect(() => { + if ( + isEqual( + multifeed.feedInfos.map(f => f.uri), + store.me.savedFeeds.all.map(f => f.uri), + ) + ) { + // no changes + return + } + multifeed.refresh() + }, [multifeed, store.me.savedFeeds.all]) + + const onPressCompose = React.useCallback(() => { + store.shell.openComposer({}) + }, [store]) + + const renderHeaderBtn = React.useCallback(() => { + return ( + <Link + href="/settings/saved-feeds" + hitSlop={10} + accessibilityRole="button" + accessibilityLabel="Edit Saved Feeds" + accessibilityHint="Opens screen to edit Saved Feeds"> + <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> + </Link> + ) + }, [pal]) + + return ( + <View style={[pal.view, styles.container]}> + <MultiFeed + scrollElRef={flatListRef} + multifeed={multifeed} + onScroll={onMainScroll} + scrollEventThrottle={100} + headerOffset={HEADER_OFFSET} + /> + <ViewHeader + title="My Feeds" + canGoBack={false} + hideOnScroll + renderButton={renderHeaderBtn} + /> + {isScrolledDown ? ( + <LoadLatestBtn + onPress={onSoftReset} + label="Scroll to top" + showIndicator={false} + /> + ) : null} + <FAB + testID="composeFAB" + onPress={onPressCompose} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + accessibilityRole="button" + accessibilityLabel="Compose post" + accessibilityHint="" + /> + </View> + ) + }), +) + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index dac554710..103b18c70 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -118,7 +118,11 @@ export const SavedFeeds = withAuthRequired( pal.border, isDesktopWeb && styles.desktopContainer, ]}> - <ViewHeader title="My Feeds" showOnDesktop showBorder={!isDesktopWeb} /> + <ViewHeader + title="Edit My Feeds" + showOnDesktop + showBorder={!isDesktopWeb} + /> <DraggableFlatList containerStyle={[!isDesktopWeb && s.flex1]} data={savedFeeds.all} diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 5617cd5b8..57f4ee696 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -30,6 +30,7 @@ import { MoonIcon, UserIconSolid, SatelliteDishIcon, + SatelliteDishIconSolid, HandIcon, } from 'lib/icons' import {UserAvatar} from 'view/com/util/UserAvatar' @@ -50,7 +51,7 @@ export const DrawerContent = observer(() => { const store = useStores() const navigation = useNavigation<NavigationProp>() const {track} = useAnalytics() - const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} = + const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = useNavigationTabState() const {notifications} = store.me @@ -97,11 +98,10 @@ export const DrawerContent = observer(() => { onPressTab('MyProfile') }, [onPressTab]) - const onPressMyFeeds = React.useCallback(() => { - track('Menu:ItemClicked', {url: 'MyFeeds'}) - navigation.navigate('SavedFeeds') - store.shell.closeDrawer() - }, [navigation, track, store.shell]) + const onPressMyFeeds = React.useCallback( + () => onPressTab('Feeds'), + [onPressTab], + ) const onPressModeration = React.useCallback(() => { track('Menu:ItemClicked', {url: 'Moderation'}) @@ -240,11 +240,19 @@ export const DrawerContent = observer(() => { /> <MenuItem icon={ - <SatelliteDishIcon - strokeWidth={1.5} - style={pal.text as FontAwesomeIconStyle} - size={24} - /> + isAtFeeds ? ( + <SatelliteDishIconSolid + strokeWidth={1.5} + style={pal.text as FontAwesomeIconStyle} + size={24} + /> + ) : ( + <SatelliteDishIcon + strokeWidth={1.5} + style={pal.text as FontAwesomeIconStyle} + size={24} + /> + ) } label="My Feeds" accessibilityLabel="My Feeds" diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 394aef7af..e8cba9047 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -18,6 +18,8 @@ import { HomeIconSolid, MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, + SatelliteDishIcon, + SatelliteDishIconSolid, BellIcon, BellIconSolid, } from 'lib/icons' @@ -33,7 +35,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() const {track} = useAnalytics() - const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} = + const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = useNavigationTabState() const {footerMinimalShellTransform} = useMinimalShellMode() @@ -59,6 +61,10 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { () => onPressTab('Search'), [onPressTab], ) + const onPressFeeds = React.useCallback( + () => onPressTab('Feeds'), + [onPressTab], + ) const onPressNotifications = React.useCallback( () => onPressTab('Notifications'), [onPressTab], @@ -121,6 +127,28 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { accessibilityHint="" /> <Btn + testID="bottomBarFeedsBtn" + icon={ + isAtFeeds ? ( + <SatelliteDishIconSolid + size={25} + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} + strokeWidth={1.8} + /> + ) : ( + <SatelliteDishIcon + size={25} + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} + strokeWidth={1.8} + /> + ) + } + onPress={onPressFeeds} + accessibilityRole="tab" + accessibilityLabel="Feeds" + accessibilityHint="" + /> + <Btn testID="bottomBarNotificationsBtn" icon={ isAtNotifications ? ( diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index e62b47ca9..3b14d7e99 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -207,7 +207,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { label="Notifications" /> <NavItem - href="/settings/saved-feeds" + href="/feeds" icon={ <SatelliteDishIcon strokeWidth={1.75} |