using register

This commit is contained in:
João Ferreiro
2022-12-12 16:30:52 +00:00
parent 3aca890da6
commit e28093ec35
60 changed files with 3620 additions and 3452 deletions
+58 -59
View File
@@ -10,76 +10,75 @@ import {ErrorMessage} from '../util/ErrorMessage'
import {Link} from '../util/Link'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
import {register} from 'react-native-bundle-splitter'
export const PostRepostedBy = observer(function PostRepostedBy({
uri,
}: {
uri: string
}) {
const store = useStores()
const [view, setView] = useState<RepostedByViewModel | undefined>()
export const PostRepostedBy = register(
observer(function PostRepostedBy({uri}: {uri: string}) {
const store = useStores()
const [view, setView] = useState<RepostedByViewModel | undefined>()
useEffect(() => {
if (view?.params.uri === uri) {
console.log('Reposted by doing nothing')
return // no change needed? or trigger refresh?
useEffect(() => {
if (view?.params.uri === uri) {
console.log('Reposted by doing nothing')
return // no change needed? or trigger refresh?
}
console.log('Fetching Reposted by', uri)
const newView = new RepostedByViewModel(store, {uri})
setView(newView)
newView
.setup()
.catch(err => console.error('Failed to fetch reposted by', err))
}, [uri, view?.params.uri, store])
const onRefresh = () => {
view?.refresh()
}
console.log('Fetching Reposted by', uri)
const newView = new RepostedByViewModel(store, {uri})
setView(newView)
newView
.setup()
.catch(err => console.error('Failed to fetch reposted by', err))
}, [uri, view?.params.uri, store])
const onRefresh = () => {
view?.refresh()
}
// loading
// =
if (
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.uri !== uri
) {
return (
<View>
<ActivityIndicator />
</View>
)
}
// loading
// =
if (
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.uri !== uri
) {
return (
<View>
<ActivityIndicator />
</View>
// error
// =
if (view.hasError) {
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
<RepostedByItem item={item} />
)
}
// error
// =
if (view.hasError) {
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
<FlatList
data={view.repostedBy}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
/>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
<RepostedByItem item={item} />
)
return (
<View>
<FlatList
data={view.repostedBy}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
/>
</View>
)
})
}),
)
const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
return (
+74 -71
View File
@@ -8,85 +8,88 @@ import {
import {useStores} from '../../../state'
import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/ErrorMessage'
import {register} from 'react-native-bundle-splitter'
export const PostThread = observer(function PostThread({
uri,
view,
}: {
uri: string
view: PostThreadViewModel
}) {
const ref = useRef<FlatList>(null)
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
const onRefresh = () => {
view?.refresh().catch(err => console.error('Failed to refresh', err))
}
const onLayout = () => {
const index = posts.findIndex(post => post._isHighlightedPost)
if (index !== -1) {
ref.current?.scrollToIndex({
index,
export const PostThread = register(
observer(function PostThread({
uri,
view,
}: {
uri: string
view: PostThreadViewModel
}) {
const ref = useRef<FlatList>(null)
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
const onRefresh = () => {
view?.refresh().catch(err => console.error('Failed to refresh', err))
}
const onLayout = () => {
const index = posts.findIndex(post => post._isHighlightedPost)
if (index !== -1) {
ref.current?.scrollToIndex({
index,
animated: false,
viewOffset: 40,
})
}
}
const onScrollToIndexFailed = (info: {
index: number
highestMeasuredFrameIndex: number
averageItemLength: number
}) => {
ref.current?.scrollToOffset({
animated: false,
viewOffset: 40,
offset: info.averageItemLength * info.index,
})
}
}
const onScrollToIndexFailed = (info: {
index: number
highestMeasuredFrameIndex: number
averageItemLength: number
}) => {
ref.current?.scrollToOffset({
animated: false,
offset: info.averageItemLength * info.index,
})
}
// loading
// =
if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) {
return (
<View>
<ActivityIndicator />
</View>
// loading
// =
if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) {
return (
<View>
<ActivityIndicator />
</View>
)
}
// error
// =
if (view.hasError) {
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
<PostThreadItem item={item} onPostReply={onRefresh} />
)
}
// error
// =
if (view.hasError) {
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View>
<FlatList
ref={ref}
data={posts}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshing={view.isRefreshing}
onRefresh={onRefresh}
onLayout={onLayout}
onScrollToIndexFailed={onScrollToIndexFailed}
style={{flex: 1}}
contentContainerStyle={{paddingBottom: 200}}
/>
)
}
// loaded
// =
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
<PostThreadItem item={item} onPostReply={onRefresh} />
)
return (
<FlatList
ref={ref}
data={posts}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshing={view.isRefreshing}
onRefresh={onRefresh}
onLayout={onLayout}
onScrollToIndexFailed={onScrollToIndexFailed}
style={{flex: 1}}
contentContainerStyle={{paddingBottom: 200}}
/>
)
})
}),
)
function* flattenThread(
post: PostThreadViewPostModel,
+253 -244
View File
@@ -18,95 +18,240 @@ import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls'
import {ComposePrompt} from '../composer/Prompt'
import {register} from 'react-native-bundle-splitter'
const PARENT_REPLY_LINE_LENGTH = 8
const REPLYING_TO_LINE_LENGTH = 6
export const PostThreadItem = observer(function PostThreadItem({
item,
onPostReply,
}: {
item: PostThreadViewPostModel
onPostReply: () => void
}) {
const store = useStores()
const [deleted, setDeleted] = useState(false)
const record = item.record as unknown as PostType.Record
const hasEngagement = item.upvoteCount || item.repostCount
export const PostThreadItem = register(
observer(function PostThreadItem({
item,
onPostReply,
}: {
item: PostThreadViewPostModel
onPostReply: () => void
}) {
const store = useStores()
const [deleted, setDeleted] = useState(false)
const record = item.record as unknown as PostType.Record
const hasEngagement = item.upvoteCount || item.repostCount
const itemHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}`
}, [item.uri, item.author.handle])
const itemTitle = `Post by ${item.author.handle}`
const authorHref = `/profile/${item.author.handle}`
const authorTitle = item.author.handle
const upvotesHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by`
}, [item.uri, item.author.handle])
const upvotesTitle = 'Upvotes on this post'
const repostsHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by`
}, [item.uri, item.author.handle])
const repostsTitle = 'Reposts of this post'
const itemHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}`
}, [item.uri, item.author.handle])
const itemTitle = `Post by ${item.author.handle}`
const authorHref = `/profile/${item.author.handle}`
const authorTitle = item.author.handle
const upvotesHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by`
}, [item.uri, item.author.handle])
const upvotesTitle = 'Upvotes on this post'
const repostsHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by`
}, [item.uri, item.author.handle])
const repostsTitle = 'Reposts of this post'
const onPressReply = () => {
store.shell.openComposer({
replyTo: {
uri: item.uri,
cid: item.cid,
text: item.record.text as string,
author: {
handle: item.author.handle,
displayName: item.author.displayName,
avatar: item.author.avatar,
const onPressReply = () => {
store.shell.openComposer({
replyTo: {
uri: item.uri,
cid: item.cid,
text: item.record.text as string,
author: {
handle: item.author.handle,
displayName: item.author.displayName,
avatar: item.author.avatar,
},
},
},
onPost: onPostReply,
})
}
const onPressToggleRepost = () => {
item
.toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e))
}
const onPressToggleUpvote = () => {
item
.toggleUpvote()
.catch(e => console.error('Failed to toggle upvote', record, e))
}
const onCopyPostText = () => {
Clipboard.setString(record.text)
Toast.show('Copied to clipboard')
}
const onDeletePost = () => {
item.delete().then(
() => {
setDeleted(true)
Toast.show('Post deleted')
},
e => {
console.error(e)
Toast.show('Failed to delete post, please try again')
},
)
}
onPost: onPostReply,
})
}
const onPressToggleRepost = () => {
item
.toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e))
}
const onPressToggleUpvote = () => {
item
.toggleUpvote()
.catch(e => console.error('Failed to toggle upvote', record, e))
}
const onCopyPostText = () => {
Clipboard.setString(record.text)
Toast.show('Copied to clipboard')
}
const onDeletePost = () => {
item.delete().then(
() => {
setDeleted(true)
Toast.show('Post deleted')
},
e => {
console.error(e)
Toast.show('Failed to delete post, please try again')
},
)
}
if (deleted) {
return (
<View style={[styles.outer, s.p20, s.flexRow]}>
<FontAwesomeIcon icon={['far', 'trash-can']} style={[s.gray4]} />
<Text style={[s.gray5, s.ml10]}>This post has been deleted.</Text>
</View>
)
}
if (deleted) {
return (
<View style={[styles.outer, s.p20, s.flexRow]}>
<FontAwesomeIcon icon={['far', 'trash-can']} style={[s.gray4]} />
<Text style={[s.gray5, s.ml10]}>This post has been deleted.</Text>
</View>
)
}
if (item._isHighlightedPost) {
return (
<>
<View style={styles.outer}>
if (item._isHighlightedPost) {
return (
<>
<View style={styles.outer}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
<UserAvatar
size={50}
displayName={item.author.displayName}
handle={item.author.handle}
avatar={item.author.avatar}
/>
</Link>
</View>
<View style={styles.layoutContent}>
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f16, s.bold]} numberOfLines={1}>
{item.author.displayName || item.author.handle}
</Text>
</Link>
<Text style={[styles.metaItem, s.f15, s.gray5]}>
&middot; {ago(item.indexedAt)}
</Text>
<View style={s.flex1} />
<PostDropdownBtn
style={styles.metaItem}
itemHref={itemHref}
itemTitle={itemTitle}
isAuthor={item.author.did === store.me.did}
onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost}>
<FontAwesomeIcon
icon="ellipsis-h"
size={14}
style={[s.mt2, s.mr5]}
/>
</PostDropdownBtn>
</View>
<View style={styles.meta}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{item.author.handle}
</Text>
</Link>
</View>
</View>
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<View
style={[
styles.postTextContainer,
styles.postTextLargeContainer,
]}>
<RichText
text={record.text}
entities={record.entities}
style={[styles.postText, styles.postTextLarge]}
/>
</View>
<PostEmbeds entities={record.entities} style={s.mb10} />
{item._isHighlightedPost && hasEngagement ? (
<View style={styles.expandedInfo}>
{item.repostCount ? (
<Link
style={styles.expandedInfoItem}
href={repostsHref}
title={repostsTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}>
{item.repostCount}
</Text>{' '}
{pluralize(item.repostCount, 'repost')}
</Text>
</Link>
) : (
<></>
)}
{item.upvoteCount ? (
<Link
style={styles.expandedInfoItem}
href={upvotesHref}
title={upvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}>
{item.upvoteCount}
</Text>{' '}
{pluralize(item.upvoteCount, 'upvote')}
</Text>
</Link>
) : (
<></>
)}
</View>
) : (
<></>
)}
<View style={[s.pl10, s.pb5]}>
<PostCtrls
big
isReposted={!!item.myState.repost}
isUpvoted={!!item.myState.upvote}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote}
/>
</View>
</View>
</View>
<ComposePrompt
noAvi
text="Write your reply"
btn="Reply"
onPressCompose={onPressReply}
/>
</>
)
} else {
return (
<Link style={styles.outer} href={itemHref} title={itemTitle}>
{!item.replyingTo && item.record.reply && (
<View style={styles.parentReplyLine} />
)}
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
{item.replyingTo ? (
<View style={styles.replyingTo}>
<View style={styles.replyingToLine} />
<View style={styles.replyingToAvatar}>
<UserAvatar
handle={item.replyingTo.author.handle}
displayName={item.replyingTo.author.displayName}
avatar={item.replyingTo.author.avatar}
size={30}
/>
</View>
<Text style={styles.replyingToText} numberOfLines={2}>
{item.replyingTo.text}
</Text>
</View>
) : undefined}
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
@@ -119,94 +264,32 @@ export const PostThreadItem = observer(function PostThreadItem({
</Link>
</View>
<View style={styles.layoutContent}>
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f16, s.bold]} numberOfLines={1}>
{item.author.displayName || item.author.handle}
</Text>
</Link>
<Text style={[styles.metaItem, s.f15, s.gray5]}>
&middot; {ago(item.indexedAt)}
</Text>
<View style={s.flex1} />
<PostDropdownBtn
style={styles.metaItem}
itemHref={itemHref}
itemTitle={itemTitle}
isAuthor={item.author.did === store.me.did}
onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost}>
<FontAwesomeIcon
icon="ellipsis-h"
size={14}
style={[s.mt2, s.mr5]}
/>
</PostDropdownBtn>
</View>
<View style={styles.meta}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{item.author.handle}
</Text>
</Link>
</View>
</View>
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<View
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
<RichText
text={record.text}
entities={record.entities}
style={[styles.postText, styles.postTextLarge]}
<PostMeta
itemHref={itemHref}
itemTitle={itemTitle}
authorHref={authorHref}
authorHandle={item.author.handle}
authorDisplayName={item.author.displayName}
timestamp={item.indexedAt}
isAuthor={item.author.did === store.me.did}
onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost}
/>
</View>
<PostEmbeds entities={record.entities} style={s.mb10} />
{item._isHighlightedPost && hasEngagement ? (
<View style={styles.expandedInfo}>
{item.repostCount ? (
<Link
style={styles.expandedInfoItem}
href={repostsHref}
title={repostsTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}>
{item.repostCount}
</Text>{' '}
{pluralize(item.repostCount, 'repost')}
</Text>
</Link>
) : (
<></>
)}
{item.upvoteCount ? (
<Link
style={styles.expandedInfoItem}
href={upvotesHref}
title={upvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}>
{item.upvoteCount}
</Text>{' '}
{pluralize(item.upvoteCount, 'upvote')}
</Text>
</Link>
) : (
<></>
)}
<View style={styles.postTextContainer}>
<RichText
text={record.text}
entities={record.entities}
style={[styles.postText]}
/>
</View>
) : (
<></>
)}
<View style={[s.pl10, s.pb5]}>
<PostEmbeds
entities={record.entities}
style={{marginBottom: 10}}
/>
<PostCtrls
big
replyCount={item.replyCount}
repostCount={item.repostCount}
upvoteCount={item.upvoteCount}
isReposted={!!item.myState.repost}
isUpvoted={!!item.myState.upvote}
onPressReply={onPressReply}
@@ -215,85 +298,11 @@ export const PostThreadItem = observer(function PostThreadItem({
/>
</View>
</View>
</View>
<ComposePrompt
noAvi
text="Write your reply"
btn="Reply"
onPressCompose={onPressReply}
/>
</>
)
} else {
return (
<Link style={styles.outer} href={itemHref} title={itemTitle}>
{!item.replyingTo && item.record.reply && (
<View style={styles.parentReplyLine} />
)}
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
{item.replyingTo ? (
<View style={styles.replyingTo}>
<View style={styles.replyingToLine} />
<View style={styles.replyingToAvatar}>
<UserAvatar
handle={item.replyingTo.author.handle}
displayName={item.replyingTo.author.displayName}
avatar={item.replyingTo.author.avatar}
size={30}
/>
</View>
<Text style={styles.replyingToText} numberOfLines={2}>
{item.replyingTo.text}
</Text>
</View>
) : undefined}
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
<UserAvatar
size={50}
displayName={item.author.displayName}
handle={item.author.handle}
avatar={item.author.avatar}
/>
</Link>
</View>
<View style={styles.layoutContent}>
<PostMeta
itemHref={itemHref}
itemTitle={itemTitle}
authorHref={authorHref}
authorHandle={item.author.handle}
authorDisplayName={item.author.displayName}
timestamp={item.indexedAt}
isAuthor={item.author.did === store.me.did}
onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost}
/>
<View style={styles.postTextContainer}>
<RichText
text={record.text}
entities={record.entities}
style={[styles.postText]}
/>
</View>
<PostEmbeds entities={record.entities} style={{marginBottom: 10}} />
<PostCtrls
replyCount={item.replyCount}
repostCount={item.repostCount}
upvoteCount={item.upvoteCount}
isReposted={!!item.myState.repost}
isUpvoted={!!item.myState.upvote}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote}
/>
</View>
</View>
</Link>
)
}
})
</Link>
)
}
}),
)
const styles = StyleSheet.create({
outer: {
+64 -59
View File
@@ -10,76 +10,81 @@ import {ErrorMessage} from '../util/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
import {register} from 'react-native-bundle-splitter'
export const PostVotedBy = observer(function PostVotedBy({
uri,
direction,
}: {
uri: string
direction: 'up' | 'down'
}) {
const store = useStores()
const [view, setView] = useState<VotesViewModel | undefined>()
export const PostVotedBy = register(
observer(function PostVotedBy({
uri,
direction,
}: {
uri: string
direction: 'up' | 'down'
}) {
const store = useStores()
const [view, setView] = useState<VotesViewModel | undefined>()
useEffect(() => {
if (view?.params.uri === uri) {
console.log('Voted by doing nothing')
return // no change needed? or trigger refresh?
useEffect(() => {
if (view?.params.uri === uri) {
console.log('Voted by doing nothing')
return // no change needed? or trigger refresh?
}
console.log('Fetching voted by', uri)
const newView = new VotesViewModel(store, {uri, direction})
setView(newView)
newView
.setup()
.catch(err => console.error('Failed to fetch voted by', err))
}, [uri, view?.params.uri, store])
const onRefresh = () => {
view?.refresh()
}
console.log('Fetching voted by', uri)
const newView = new VotesViewModel(store, {uri, direction})
setView(newView)
newView.setup().catch(err => console.error('Failed to fetch voted by', err))
}, [uri, view?.params.uri, store])
const onRefresh = () => {
view?.refresh()
}
// loading
// =
if (
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.uri !== uri
) {
return (
<View>
<ActivityIndicator />
</View>
)
}
// loading
// =
if (
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.uri !== uri
) {
return (
<View>
<ActivityIndicator />
</View>
// error
// =
if (view.hasError) {
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
/>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: VotesViewItemModel}) => (
<LikedByItem item={item} />
)
}
// error
// =
if (view.hasError) {
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}
<FlatList
data={view.votes}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
/>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: VotesViewItemModel}) => (
<LikedByItem item={item} />
)
return (
<View>
<FlatList
data={view.votes}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
/>
</View>
)
})
}),
)
const LikedByItem = ({item}: {item: VotesViewItemModel}) => {
return (