Add feeds tab
This commit is contained in:
parent
df6d249e85
commit
257686f360
17 changed files with 937 additions and 290 deletions
|
@ -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>
|
||||
|
|
|
@ -131,7 +131,7 @@ const styles = isDesktopWeb
|
|||
backgroundColor: 'transparent',
|
||||
},
|
||||
contentContainer: {
|
||||
columnGap: 16,
|
||||
columnGap: 20,
|
||||
marginLeft: 18,
|
||||
paddingRight: 28,
|
||||
backgroundColor: 'transparent',
|
||||
|
|
|
@ -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}
|
||||
|
|
230
src/view/com/posts/MultiFeed.tsx
Normal file
230
src/view/com/posts/MultiFeed.tsx
Normal file
|
@ -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,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue