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:
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
@@ -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,17 +43,20 @@ export const PostThread = observer(function PostThread({
|
|||||||
viewOffset: 40,
|
viewOffset: 40,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}, [posts, ref])
|
||||||
const onScrollToIndexFailed = (info: {
|
const onScrollToIndexFailed = React.useCallback(
|
||||||
index: number
|
(info: {
|
||||||
highestMeasuredFrameIndex: number
|
index: number
|
||||||
averageItemLength: number
|
highestMeasuredFrameIndex: number
|
||||||
}) => {
|
averageItemLength: number
|
||||||
ref.current?.scrollToOffset({
|
}) => {
|
||||||
animated: false,
|
ref.current?.scrollToOffset({
|
||||||
offset: info.averageItemLength * info.index,
|
animated: false,
|
||||||
})
|
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}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user