Fix to scrolling to posts within a thread (#228)

* Fix: render the entire thread at start so that scrollToIndex works always (close #270)

* Visual fixes to thread 'load more'

* A few small perf improvements to thread rendering

* Fix lint
This commit is contained in:
Paul Frazee
2023-02-21 15:32:38 -06:00
committed by GitHub
parent f55fbe73c7
commit b1ffb1e686
3 changed files with 61 additions and 45 deletions
+13 -12
View File
@@ -322,7 +322,7 @@ export class PostThreadViewModel {
} }
private _replaceAll(res: GetPostThread.Response) { private _replaceAll(res: GetPostThread.Response) {
// sortThread(res.data.thread) TODO needed? sortThread(res.data.thread)
const keyGen = reactKeyGenerator() const keyGen = reactKeyGenerator()
const thread = new PostThreadViewPostModel( const thread = new PostThreadViewPostModel(
this.rootStore, this.rootStore,
@@ -338,36 +338,37 @@ export class PostThreadViewModel {
} }
} }
/* type MaybePost =
TODO needed? | GetPostThread.ThreadViewPost
| GetPostThread.NotFoundPost
| {[k: string]: unknown; $type: string}
function sortThread(post: MaybePost) { function sortThread(post: MaybePost) {
if (post.notFound) { if (post.notFound) {
return return
} }
post = post as GetPostThread.Post post = post as GetPostThread.ThreadViewPost
if (post.replies) { if (post.replies) {
post.replies.sort((a: MaybePost, b: MaybePost) => { post.replies.sort((a: MaybePost, b: MaybePost) => {
post = post as GetPostThread.Post post = post as GetPostThread.ThreadViewPost
if (a.notFound) { if (a.notFound) {
return 1 return 1
} }
if (b.notFound) { if (b.notFound) {
return -1 return -1
} }
a = a as GetPostThread.Post a = a as GetPostThread.ThreadViewPost
b = b as GetPostThread.Post b = b as GetPostThread.ThreadViewPost
const aIsByOp = a.author.did === post.author.did const aIsByOp = a.post.author.did === post.post.author.did
const bIsByOp = b.author.did === post.author.did const bIsByOp = b.post.author.did === post.post.author.did
if (aIsByOp && bIsByOp) { if (aIsByOp && bIsByOp) {
return a.indexedAt.localeCompare(b.indexedAt) // oldest return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
} else if (aIsByOp) { } else if (aIsByOp) {
return -1 // op's own reply return -1 // op's own reply
} else if (bIsByOp) { } else if (bIsByOp) {
return 1 // op's own reply return 1 // op's own reply
} }
return b.indexedAt.localeCompare(a.indexedAt) // newest return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
}) })
post.replies.forEach(reply => sortThread(reply)) post.replies.forEach(reply => sortThread(reply))
} }
} }
*/
+17 -7
View File
@@ -18,8 +18,14 @@ export const PostThread = observer(function PostThread({
}) { }) {
const ref = useRef<FlatList>(null) const ref = useRef<FlatList>(null)
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] const posts = React.useMemo(
const onRefresh = async () => { () => (view.thread ? Array.from(flattenThread(view.thread)) : []),
[view.thread],
)
// events
// =
const onRefresh = React.useCallback(async () => {
setIsRefreshing(true) setIsRefreshing(true)
try { try {
view?.refresh() view?.refresh()
@@ -27,8 +33,8 @@ export const PostThread = observer(function PostThread({
view.rootStore.log.error('Failed to refresh posts thread', err) view.rootStore.log.error('Failed to refresh posts thread', err)
} }
setIsRefreshing(false) setIsRefreshing(false)
} }, [view, setIsRefreshing])
const onLayout = () => { const onLayout = React.useCallback(() => {
const index = posts.findIndex(post => post._isHighlightedPost) const index = posts.findIndex(post => post._isHighlightedPost)
if (index !== -1) { if (index !== -1) {
ref.current?.scrollToIndex({ ref.current?.scrollToIndex({
@@ -37,8 +43,9 @@ export const PostThread = observer(function PostThread({
viewOffset: 40, viewOffset: 40,
}) })
} }
} }, [posts, ref])
const onScrollToIndexFailed = (info: { const onScrollToIndexFailed = React.useCallback(
(info: {
index: number index: number
highestMeasuredFrameIndex: number highestMeasuredFrameIndex: number
averageItemLength: number averageItemLength: number
@@ -47,7 +54,9 @@ export const PostThread = observer(function PostThread({
animated: false, animated: false,
offset: info.averageItemLength * info.index, offset: info.averageItemLength * info.index,
}) })
} },
[ref],
)
// loading // loading
// = // =
@@ -78,6 +87,7 @@ export const PostThread = observer(function PostThread({
<FlatList <FlatList
ref={ref} ref={ref}
data={posts} data={posts}
initialNumToRender={posts.length}
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
refreshing={isRefreshing} refreshing={isRefreshing}
+23 -18
View File
@@ -1,4 +1,4 @@
import React, {useMemo, useState} from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
@@ -32,36 +32,36 @@ export const PostThreadItem = observer(function PostThreadItem({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const [deleted, setDeleted] = useState(false) const [deleted, setDeleted] = React.useState(false)
const record = item.postRecord const record = item.postRecord
const hasEngagement = item.post.upvoteCount || item.post.repostCount const hasEngagement = item.post.upvoteCount || item.post.repostCount
const itemUri = item.post.uri const itemUri = item.post.uri
const itemCid = item.post.cid const itemCid = item.post.cid
const itemHref = useMemo(() => { const itemHref = React.useMemo(() => {
const urip = new AtUri(item.post.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.post.author.handle}/post/${urip.rkey}` return `/profile/${item.post.author.handle}/post/${urip.rkey}`
}, [item.post.uri, item.post.author.handle]) }, [item.post.uri, item.post.author.handle])
const itemTitle = `Post by ${item.post.author.handle}` const itemTitle = `Post by ${item.post.author.handle}`
const authorHref = `/profile/${item.post.author.handle}` const authorHref = `/profile/${item.post.author.handle}`
const authorTitle = item.post.author.handle const authorTitle = item.post.author.handle
const upvotesHref = useMemo(() => { const upvotesHref = React.useMemo(() => {
const urip = new AtUri(item.post.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by` return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by`
}, [item.post.uri, item.post.author.handle]) }, [item.post.uri, item.post.author.handle])
const upvotesTitle = 'Likes on this post' const upvotesTitle = 'Likes on this post'
const repostsHref = useMemo(() => { const repostsHref = React.useMemo(() => {
const urip = new AtUri(item.post.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by` return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by`
}, [item.post.uri, item.post.author.handle]) }, [item.post.uri, item.post.author.handle])
const repostsTitle = 'Reposts of this post' const repostsTitle = 'Reposts of this post'
const onPressReply = () => { const onPressReply = React.useCallback(() => {
store.shell.openComposer({ store.shell.openComposer({
replyTo: { replyTo: {
uri: item.post.uri, uri: item.post.uri,
cid: item.post.cid, cid: item.post.cid,
text: record.text as string, text: record?.text as string,
author: { author: {
handle: item.post.author.handle, handle: item.post.author.handle,
displayName: item.post.author.displayName, displayName: item.post.author.displayName,
@@ -70,22 +70,22 @@ export const PostThreadItem = observer(function PostThreadItem({
}, },
onPost: onPostReply, onPost: onPostReply,
}) })
} }, [store, item, record, onPostReply])
const onPressToggleRepost = () => { const onPressToggleRepost = React.useCallback(() => {
return item return item
.toggleRepost() .toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e)) .catch(e => store.log.error('Failed to toggle repost', e))
} }, [item, store])
const onPressToggleUpvote = () => { const onPressToggleUpvote = React.useCallback(() => {
return item return item
.toggleUpvote() .toggleUpvote()
.catch(e => store.log.error('Failed to toggle upvote', e)) .catch(e => store.log.error('Failed to toggle upvote', e))
} }, [item, store])
const onCopyPostText = () => { const onCopyPostText = React.useCallback(() => {
Clipboard.setString(record?.text || '') Clipboard.setString(record?.text || '')
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard')
} }, [record])
const onDeletePost = () => { const onDeletePost = React.useCallback(() => {
item.delete().then( item.delete().then(
() => { () => {
setDeleted(true) setDeleted(true)
@@ -96,7 +96,7 @@ export const PostThreadItem = observer(function PostThreadItem({
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )
} }, [item, store])
if (!record) { if (!record) {
return <ErrorMessage message="Invalid or unsupported post record" /> return <ErrorMessage message="Invalid or unsupported post record" />
@@ -341,7 +341,8 @@ export const PostThreadItem = observer(function PostThreadItem({
href={itemHref} href={itemHref}
title={itemTitle} title={itemTitle}
noFeedback> noFeedback>
<Text style={pal.link}>Load more</Text> <Text style={pal.link}>Continue thread...</Text>
<FontAwesomeIcon icon="angle-right" style={pal.link} size={18} />
</Link> </Link>
) : undefined} ) : undefined}
</> </>
@@ -433,8 +434,12 @@ const styles = StyleSheet.create({
marginRight: 10, marginRight: 10,
}, },
loadMore: { loadMore: {
flexDirection: 'row',
justifyContent: 'space-between',
borderTopWidth: 1, borderTopWidth: 1,
paddingLeft: 28, paddingLeft: 80,
paddingRight: 20,
paddingVertical: 10, paddingVertical: 10,
marginBottom: 8,
}, },
}) })