diff options
Diffstat (limited to 'src/view/com/post-thread')
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 88 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 176 |
2 files changed, 264 insertions, 0 deletions
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx new file mode 100644 index 000000000..bc6642e07 --- /dev/null +++ b/src/view/com/post-thread/PostThread.tsx @@ -0,0 +1,88 @@ +import React, {useState, useEffect} from 'react' +import {observer} from 'mobx-react-lite' +import {ActivityIndicator, FlatList, Text, View} from 'react-native' +import {OnNavigateContent} from '../../routes/types' +import { + PostThreadViewModel, + PostThreadViewPostModel, +} from '../../../state/models/post-thread-view' +import {useStores} from '../../../state' +import {PostThreadItem} from './PostThreadItem' + +export const PostThread = observer(function PostThread({ + uri, + onNavigateContent, +}: { + uri: string + onNavigateContent: OnNavigateContent +}) { + const store = useStores() + const [view, setView] = useState<PostThreadViewModel | undefined>() + + useEffect(() => { + if (view?.params.uri === uri) { + console.log('Post thread doing nothing') + return // no change needed? or trigger refresh? + } + console.log('Fetching post thread', uri) + const newView = new PostThreadViewModel(store, {uri}) + setView(newView) + newView.setup().catch(err => console.error('Failed to fetch thread', err)) + }, [uri, view?.params.uri, store]) + + // not yet setup + if ( + !view || + (view.isLoading && !view.isRefreshing) || + view.params.uri !== uri + ) { + return ( + <View> + <ActivityIndicator /> + </View> + ) + } + + // error + if (view.hasError) { + return ( + <View> + <Text>{view.error}</Text> + </View> + ) + } + + // rendering + const posts = Array.from(flattenThread(view.thread)) + const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( + <PostThreadItem item={item} onNavigateContent={onNavigateContent} /> + ) + const onRefresh = () => { + view.refresh().catch(err => console.error('Failed to refresh', err)) + } + return ( + <View> + {view.isRefreshing && <ActivityIndicator />} + {view.hasContent && ( + <FlatList + data={posts} + keyExtractor={item => item._reactKey} + renderItem={renderItem} + refreshing={view.isRefreshing} + onRefresh={onRefresh} + /> + )} + </View> + ) +}) + +function* flattenThread( + post: PostThreadViewPostModel, +): Generator<PostThreadViewPostModel, void> { + yield post + if (post.replies?.length) { + for (const reply of post.replies) { + yield* flattenThread(reply) + } + } +} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx new file mode 100644 index 000000000..33857f48a --- /dev/null +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -0,0 +1,176 @@ +import React from 'react' +import {observer} from 'mobx-react-lite' +import { + Image, + ImageSourcePropType, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native' +import {bsky} from '@adxp/mock-api' +import moment from 'moment' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {OnNavigateContent} from '../../routes/types' +import {PostThreadViewPostModel} from '../../../state/models/post-thread-view' + +const IMAGES: Record<string, ImageSourcePropType> = { + 'alice.com': require('../../assets/alice.jpg'), + 'bob.com': require('../../assets/bob.jpg'), + 'carla.com': require('../../assets/carla.jpg'), +} + +export const PostThreadItem = observer(function PostThreadItem({ + item, // onNavigateContent, +}: { + item: PostThreadViewPostModel + onNavigateContent: OnNavigateContent +}) { + const record = item.record as unknown as bsky.Post.Record + const onPressOuter = () => { + // TODO onNavigateContent + } + return ( + <TouchableOpacity style={styles.outer} onPress={onPressOuter}> + <View style={styles.layout}> + <View style={styles.layoutAvi}> + <Image + style={styles.avi} + source={IMAGES[item.author.name] || IMAGES['alice.com']} + /> + </View> + <View style={styles.layoutContent}> + <View style={styles.meta}> + <Text style={[styles.metaItem, styles.metaDisplayName]}> + {item.author.displayName} + </Text> + <Text style={[styles.metaItem, styles.metaName]}> + @{item.author.name} + </Text> + <Text style={[styles.metaItem, styles.metaDate]}> + · {moment(item.indexedAt).fromNow(true)} + </Text> + </View> + <Text style={styles.postText}>{record.text}</Text> + <View style={styles.ctrls}> + <View style={styles.ctrl}> + <FontAwesomeIcon + style={styles.ctrlReplyIcon} + icon={['far', 'comment']} + /> + <Text>{item.replyCount}</Text> + </View> + <View style={styles.ctrl}> + <FontAwesomeIcon + style={styles.ctrlRepostIcon} + icon="retweet" + size={22} + /> + <Text>{item.repostCount}</Text> + </View> + <View style={styles.ctrl}> + <FontAwesomeIcon + style={styles.ctrlLikeIcon} + icon={['far', 'heart']} + /> + <Text>{item.likeCount}</Text> + </View> + <View style={styles.ctrl}> + <FontAwesomeIcon + style={styles.ctrlShareIcon} + icon="share-from-square" + /> + </View> + </View> + </View> + </View> + </TouchableOpacity> + ) +}) + +const styles = StyleSheet.create({ + outer: { + borderTopWidth: 1, + borderTopColor: '#e8e8e8', + backgroundColor: '#fff', + padding: 10, + }, + repostedBy: { + flexDirection: 'row', + paddingLeft: 70, + }, + repostedByIcon: { + marginRight: 2, + color: 'gray', + }, + repostedByText: { + color: 'gray', + fontWeight: 'bold', + fontSize: 13, + }, + layout: { + flexDirection: 'row', + }, + layoutAvi: { + width: 70, + }, + avi: { + width: 60, + height: 60, + borderRadius: 30, + resizeMode: 'cover', + }, + layoutContent: { + flex: 1, + }, + meta: { + flexDirection: 'row', + paddingTop: 2, + paddingBottom: 4, + }, + metaItem: { + paddingRight: 5, + }, + metaDisplayName: { + fontSize: 15, + fontWeight: 'bold', + }, + metaName: { + fontSize: 14, + color: 'gray', + }, + metaDate: { + fontSize: 14, + color: 'gray', + }, + postText: { + fontSize: 15, + paddingBottom: 5, + }, + ctrls: { + flexDirection: 'row', + }, + ctrl: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + paddingLeft: 4, + paddingRight: 4, + }, + ctrlReplyIcon: { + marginRight: 5, + color: 'gray', + }, + ctrlRepostIcon: { + marginRight: 5, + color: 'gray', + }, + ctrlLikeIcon: { + marginRight: 5, + color: 'gray', + }, + ctrlShareIcon: { + marginRight: 5, + color: 'gray', + }, +}) |