bsky-app/src/view/com/posts/MultiFeed.tsx
Paul Frazee 6f69157269
Post UI updates (Profile Preview on mobile) (#990)
* Update postmeta to put the timestamp on the right side on mobile

* Drop the two-line PostMeta mode

* Add ProfilePreview modal

* Tune PostMeta to give the best behavior possible for a given platform

* Remove old showFollowBtn attributes

* Fix style issue

* Switch the follow button in the profile header to use the inverted color for consistency with the rest of the app

* Fix lint

* Fix darkmode

* Tune the profile preview footer

* Better analytics choice
2023-07-06 21:12:54 -05:00

254 lines
7.1 KiB
TypeScript

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/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {isDesktopWeb} from 'platform/detection'
import {CogIcon} from 'lib/icons'
export const MultiFeed = observer(function Feed({
multifeed,
style,
scrollElRef,
onScroll,
scrollEventThrottle,
testID,
headerOffset = 0,
extraData,
}: {
multifeed: PostsMultiFeedModel
style?: StyleProp<ViewStyle>
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 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') {
if (isDesktopWeb) {
return (
<View style={[pal.view, pal.border, styles.headerDesktop]}>
<Text type="2xl-bold" style={pal.text}>
My Feeds
</Text>
<Link href="/settings/saved-feeds">
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
</Link>
</View>
)
}
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} />
} 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, pal.viewLight]} href="/search/feeds">
<FontAwesomeIcon icon="search" size={18} color={pal.colors.text} />
<Text type="xl-medium" style={pal.text}>
Discover new feeds
</Text>
</Link>
)
}
return null
},
[pal],
)
const ListFooter = React.useCallback(
() =>
multifeed.isLoading && !isRefreshing ? (
<View style={styles.loadMore}>
<ActivityIndicator color={pal.colors.text} />
</View>
) : (
<View />
),
[multifeed.isLoading, isRefreshing, pal],
)
const ListHeader = React.useCallback(() => {
return (
<Link style={[styles.footerLink, pal.viewLight]} href="/search/feeds">
<FontAwesomeIcon icon="search" size={18} color={pal.colors.text} />
<Text type="xl-medium" style={pal.text}>
Discover new feeds
</Text>
</Link>
)
}, [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={ListFooter}
ListHeaderComponent={ListHeader}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={s.contentContainer}
style={[{paddingTop: headerOffset}, pal.view, 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,
},
headerDesktop: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
marginBottom: 4,
paddingHorizontal: 16,
paddingVertical: 8,
},
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,
marginVertical: 8,
gap: 8,
},
loadMore: {
paddingTop: 10,
},
})