diff options
Diffstat (limited to 'src/view/com/posts')
-rw-r--r-- | src/view/com/posts/ComposerPrompt.tsx | 46 | ||||
-rw-r--r-- | src/view/com/posts/ComposerPrompt.web.tsx | 4 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 157 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 27 |
4 files changed, 121 insertions, 113 deletions
diff --git a/src/view/com/posts/ComposerPrompt.tsx b/src/view/com/posts/ComposerPrompt.tsx index 1ddc28756..c367a17fc 100644 --- a/src/view/com/posts/ComposerPrompt.tsx +++ b/src/view/com/posts/ComposerPrompt.tsx @@ -1,47 +1,5 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {Text} from '../util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' - -export function ComposerPrompt({ - onPressCompose, -}: { +export function ComposerPrompt(_opts: { onPressCompose: (imagesOpen?: boolean) => void }) { - const pal = usePalette('default') - return ( - <View style={[pal.view, pal.border, styles.container]}> - <TouchableOpacity - testID="composePromptButton" - onPress={() => onPressCompose(false)} - style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}> - <Text type="button" style={pal.text}> - New post - </Text> - </TouchableOpacity> - <TouchableOpacity - onPress={() => onPressCompose(true)} - style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}> - <Text type="button" style={pal.text}> - Share photo - </Text> - </TouchableOpacity> - </View> - ) + return null } - -const styles = StyleSheet.create({ - container: { - paddingVertical: 12, - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center', - borderTopWidth: 1, - }, - btn: { - paddingVertical: 6, - paddingHorizontal: 14, - borderRadius: 30, - marginRight: 10, - }, -}) diff --git a/src/view/com/posts/ComposerPrompt.web.tsx b/src/view/com/posts/ComposerPrompt.web.tsx index 96c09f0b3..a87653cf8 100644 --- a/src/view/com/posts/ComposerPrompt.web.tsx +++ b/src/view/com/posts/ComposerPrompt.web.tsx @@ -1,8 +1,8 @@ import React from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {Text} from '../util/text/Text' -import {usePalette} from '../../lib/hooks/usePalette' -import {s} from '../../lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' export function ComposerPrompt({ onPressCompose, diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 409ce4af2..57363ca51 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -11,103 +11,144 @@ import {CenteredView, FlatList} from '../util/Views' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {EmptyState} from '../util/EmptyState' import {ErrorMessage} from '../util/error/ErrorMessage' -import {FeedModel} from '../../../state/models/feed-view' +import {FeedModel} from 'state/models/feed-view' import {FeedItem} from './FeedItem' import {ComposerPrompt} from './ComposerPrompt' -import {OnScrollCb} from '../../lib/hooks/useOnMainScroll' -import {s} from '../../lib/styles' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useAnalytics} from 'lib/analytics' const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} +const ERROR_FEED_ITEM = {_reactKey: '__error__'} export const Feed = observer(function Feed({ feed, style, scrollElRef, - onPressCompose, onPressTryAgain, + onPressCompose, onScroll, testID, + headerOffset = 0, }: { feed: FeedModel style?: StyleProp<ViewStyle> scrollElRef?: MutableRefObject<FlatList<any> | null> - onPressCompose: (imagesOpen?: boolean) => void onPressTryAgain?: () => void + onPressCompose: (imagesOpen?: boolean) => void onScroll?: OnScrollCb testID?: string + headerOffset?: number }) { + const {track} = useAnalytics() + const [isRefreshing, setIsRefreshing] = React.useState(false) + + const data = React.useMemo(() => { + let feedItems: any[] = [] + if (feed.hasLoaded) { + feedItems = feedItems.concat([COMPOSE_PROMPT_ITEM]) + if (feed.hasError) { + feedItems = feedItems.concat([ERROR_FEED_ITEM]) + } + if (feed.isEmpty) { + feedItems = feedItems.concat([EMPTY_FEED_ITEM]) + } else { + feedItems = feedItems.concat(feed.feed) + } + } + return feedItems + }, [feed.hasError, feed.hasLoaded, feed.isEmpty, feed.feed]) + + // events + // = + + const onRefresh = React.useCallback(async () => { + track('Feed:onRefresh') + setIsRefreshing(true) + try { + await feed.refresh() + } catch (err) { + feed.rootStore.log.error('Failed to refresh posts feed', err) + } + setIsRefreshing(false) + }, [feed, track, setIsRefreshing]) + const onEndReached = React.useCallback(async () => { + track('Feed:onEndReached') + try { + await feed.loadMore() + } catch (err) { + feed.rootStore.log.error('Failed to load more posts', err) + } + }, [feed, track]) + + // rendering + // = + // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // VirtualizedList: You have a large list that is slow to update - make sure your // renderItem function renders components that follow React performance best practices // like PureComponent, shouldComponentUpdate, etc - const renderItem = ({item}: {item: any}) => { - if (item === COMPOSE_PROMPT_ITEM) { - return <ComposerPrompt onPressCompose={onPressCompose} /> - } else if (item === EMPTY_FEED_ITEM) { - return ( - <EmptyState - icon="bars" - message="This feed is empty!" - style={styles.emptyState} - /> - ) - } else { - return <FeedItem item={item} /> - } - } - const onRefresh = () => { - feed - .refresh() - .catch(err => - feed.rootStore.log.error('Failed to refresh posts feed', err), - ) - } - const onEndReached = () => { - feed - .loadMore() - .catch(err => feed.rootStore.log.error('Failed to load more posts', err)) - } - let data - if (feed.hasLoaded) { - if (feed.isEmpty) { - data = [COMPOSE_PROMPT_ITEM, EMPTY_FEED_ITEM] - } else { - data = [COMPOSE_PROMPT_ITEM].concat(feed.feed) - } - } - const FeedFooter = () => - feed.isLoading ? ( - <View style={styles.feedFooter}> - <ActivityIndicator /> - </View> - ) : ( - <View /> - ) - return ( - <View testID={testID} style={style}> - <CenteredView> - {!data && <ComposerPrompt onPressCompose={onPressCompose} />} - {feed.isLoading && !data && <PostFeedLoadingPlaceholder />} - {feed.hasError && ( + const renderItem = React.useCallback( + ({item}: {item: any}) => { + if (item === COMPOSE_PROMPT_ITEM) { + return <ComposerPrompt onPressCompose={onPressCompose} /> + } else if (item === EMPTY_FEED_ITEM) { + return ( + <EmptyState + icon="bars" + message="This feed is empty!" + style={styles.emptyState} + /> + ) + } else if (item === ERROR_FEED_ITEM) { + return ( <ErrorMessage message={feed.error} onPressTryAgain={onPressTryAgain} /> - )} - </CenteredView> - {feed.hasLoaded && data && ( + ) + } + return <FeedItem item={item} /> + }, + [feed, onPressTryAgain, onPressCompose], + ) + + const FeedFooter = React.useCallback( + () => + feed.isLoading ? ( + <View style={styles.feedFooter}> + <ActivityIndicator /> + </View> + ) : ( + <View /> + ), + [feed], + ) + + return ( + <View testID={testID} style={style}> + {feed.isLoading && data.length === 0 && ( + <CenteredView style={{paddingTop: headerOffset}}> + <PostFeedLoadingPlaceholder /> + </CenteredView> + )} + {data.length > 0 && ( <FlatList ref={scrollElRef} data={data} keyExtractor={item => item._reactKey} renderItem={renderItem} ListFooterComponent={FeedFooter} - refreshing={feed.isRefreshing} + refreshing={isRefreshing} contentContainerStyle={s.contentContainer} onScroll={onScroll} onRefresh={onRefresh} onEndReached={onEndReached} + removeClippedSubviews={true} + contentInset={{top: headerOffset}} + contentOffset={{x: 0, y: headerOffset * -1}} + progressViewOffset={headerOffset} /> )} </View> diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index cda2ac0b0..67807b14e 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -8,7 +8,7 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {FeedItemModel} from '../../../state/models/feed-view' +import {FeedItemModel} from 'state/models/feed-view' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserInfoText} from '../util/UserInfoText' @@ -18,9 +18,10 @@ import {PostEmbeds} from '../util/PostEmbeds' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' -import {s} from '../../lib/styles' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' +import {s} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' export const FeedItem = observer(function ({ item, @@ -33,8 +34,11 @@ export const FeedItem = observer(function ({ }) { const store = useStores() const pal = usePalette('default') + const {track} = useAnalytics() const [deleted, setDeleted] = useState(false) const record = item.postRecord + const itemUri = item.post.uri + const itemCid = item.post.cid const itemHref = useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}` @@ -50,6 +54,7 @@ export const FeedItem = observer(function ({ }, [record?.reply]) const onPressReply = () => { + track('FeedItem:PostReply') store.shell.openComposer({ replyTo: { uri: item.post.uri, @@ -64,12 +69,14 @@ export const FeedItem = observer(function ({ }) } const onPressToggleRepost = () => { - item + track('FeedItem:PostRepost') + return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) } const onPressToggleUpvote = () => { - item + track('FeedItem:PostLike') + return item .toggleUpvote() .catch(e => store.log.error('Failed to toggle upvote', e)) } @@ -78,6 +85,7 @@ export const FeedItem = observer(function ({ Toast.show('Copied to clipboard') } const onDeletePost = () => { + track('FeedItem:PostDelete') item.delete().then( () => { setDeleted(true) @@ -195,12 +203,11 @@ export const FeedItem = observer(function ({ <FontAwesomeIcon icon={['far', 'eye-slash']} style={s.mr2} /> <Text type="sm">This post is by a muted account.</Text> </View> - ) : record.text ? ( + ) : item.richText?.text ? ( <View style={styles.postTextContainer}> <RichText type="post-text" - text={record.text} - entities={record.entities} + richText={item.richText} lineHeight={1.3} /> </View> @@ -210,6 +217,8 @@ export const FeedItem = observer(function ({ ) : null} <PostCtrls style={styles.ctrls} + itemUri={itemUri} + itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} isAuthor={item.post.author.did === store.me.did} |