From b1ffb1e6866e080ea14377d9b0f7db2ab8738e28 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 21 Feb 2023 15:32:38 -0600 Subject: [PATCH] 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 --- src/state/models/post-thread-view.ts | 25 +++++++------ src/view/com/post-thread/PostThread.tsx | 40 ++++++++++++-------- src/view/com/post-thread/PostThreadItem.tsx | 41 ++++++++++++--------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index 6b1e7ab9bb..8d1120ba29 100644 --- a/src/state/models/post-thread-view.ts +++ b/src/state/models/post-thread-view.ts @@ -322,7 +322,7 @@ export class PostThreadViewModel { } private _replaceAll(res: GetPostThread.Response) { - // sortThread(res.data.thread) TODO needed? + sortThread(res.data.thread) const keyGen = reactKeyGenerator() const thread = new PostThreadViewPostModel( this.rootStore, @@ -338,36 +338,37 @@ export class PostThreadViewModel { } } -/* -TODO needed? +type MaybePost = + | GetPostThread.ThreadViewPost + | GetPostThread.NotFoundPost + | {[k: string]: unknown; $type: string} function sortThread(post: MaybePost) { if (post.notFound) { return } - post = post as GetPostThread.Post + post = post as GetPostThread.ThreadViewPost if (post.replies) { post.replies.sort((a: MaybePost, b: MaybePost) => { - post = post as GetPostThread.Post + post = post as GetPostThread.ThreadViewPost if (a.notFound) { return 1 } if (b.notFound) { return -1 } - a = a as GetPostThread.Post - b = b as GetPostThread.Post - const aIsByOp = a.author.did === post.author.did - const bIsByOp = b.author.did === post.author.did + a = a as GetPostThread.ThreadViewPost + b = b as GetPostThread.ThreadViewPost + const aIsByOp = a.post.author.did === post.post.author.did + const bIsByOp = b.post.author.did === post.post.author.did if (aIsByOp && bIsByOp) { - return a.indexedAt.localeCompare(b.indexedAt) // oldest + return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest } else if (aIsByOp) { return -1 // op's own reply } else if (bIsByOp) { 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)) } } -*/ diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 72a6eba9bd..606939fe2a 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -18,8 +18,14 @@ export const PostThread = observer(function PostThread({ }) { const ref = useRef(null) const [isRefreshing, setIsRefreshing] = React.useState(false) - const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] - const onRefresh = async () => { + const posts = React.useMemo( + () => (view.thread ? Array.from(flattenThread(view.thread)) : []), + [view.thread], + ) + + // events + // = + const onRefresh = React.useCallback(async () => { setIsRefreshing(true) try { view?.refresh() @@ -27,8 +33,8 @@ export const PostThread = observer(function PostThread({ view.rootStore.log.error('Failed to refresh posts thread', err) } setIsRefreshing(false) - } - const onLayout = () => { + }, [view, setIsRefreshing]) + const onLayout = React.useCallback(() => { const index = posts.findIndex(post => post._isHighlightedPost) if (index !== -1) { ref.current?.scrollToIndex({ @@ -37,17 +43,20 @@ export const PostThread = observer(function PostThread({ viewOffset: 40, }) } - } - const onScrollToIndexFailed = (info: { - index: number - highestMeasuredFrameIndex: number - averageItemLength: number - }) => { - ref.current?.scrollToOffset({ - animated: false, - offset: info.averageItemLength * info.index, - }) - } + }, [posts, ref]) + const onScrollToIndexFailed = React.useCallback( + (info: { + index: number + highestMeasuredFrameIndex: number + averageItemLength: number + }) => { + ref.current?.scrollToOffset({ + animated: false, + offset: info.averageItemLength * info.index, + }) + }, + [ref], + ) // loading // = @@ -78,6 +87,7 @@ export const PostThread = observer(function PostThread({ item._reactKey} renderItem={renderItem} refreshing={isRefreshing} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 0974683ba5..f8a04ac3b4 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react' +import React from 'react' import {observer} from 'mobx-react-lite' import {StyleSheet, View} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' @@ -32,36 +32,36 @@ export const PostThreadItem = observer(function PostThreadItem({ }) { const pal = usePalette('default') const store = useStores() - const [deleted, setDeleted] = useState(false) + const [deleted, setDeleted] = React.useState(false) const record = item.postRecord const hasEngagement = item.post.upvoteCount || item.post.repostCount const itemUri = item.post.uri const itemCid = item.post.cid - const itemHref = useMemo(() => { + const itemHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}` }, [item.post.uri, item.post.author.handle]) const itemTitle = `Post by ${item.post.author.handle}` const authorHref = `/profile/${item.post.author.handle}` const authorTitle = item.post.author.handle - const upvotesHref = useMemo(() => { + const upvotesHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by` }, [item.post.uri, item.post.author.handle]) const upvotesTitle = 'Likes on this post' - const repostsHref = useMemo(() => { + const repostsHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by` }, [item.post.uri, item.post.author.handle]) const repostsTitle = 'Reposts of this post' - const onPressReply = () => { + const onPressReply = React.useCallback(() => { store.shell.openComposer({ replyTo: { uri: item.post.uri, cid: item.post.cid, - text: record.text as string, + text: record?.text as string, author: { handle: item.post.author.handle, displayName: item.post.author.displayName, @@ -70,22 +70,22 @@ export const PostThreadItem = observer(function PostThreadItem({ }, onPost: onPostReply, }) - } - const onPressToggleRepost = () => { + }, [store, item, record, onPostReply]) + const onPressToggleRepost = React.useCallback(() => { return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) - } - const onPressToggleUpvote = () => { + }, [item, store]) + const onPressToggleUpvote = React.useCallback(() => { return item .toggleUpvote() .catch(e => store.log.error('Failed to toggle upvote', e)) - } - const onCopyPostText = () => { + }, [item, store]) + const onCopyPostText = React.useCallback(() => { Clipboard.setString(record?.text || '') Toast.show('Copied to clipboard') - } - const onDeletePost = () => { + }, [record]) + const onDeletePost = React.useCallback(() => { item.delete().then( () => { setDeleted(true) @@ -96,7 +96,7 @@ export const PostThreadItem = observer(function PostThreadItem({ Toast.show('Failed to delete post, please try again') }, ) - } + }, [item, store]) if (!record) { return @@ -341,7 +341,8 @@ export const PostThreadItem = observer(function PostThreadItem({ href={itemHref} title={itemTitle} noFeedback> - Load more + Continue thread... + ) : undefined} @@ -433,8 +434,12 @@ const styles = StyleSheet.create({ marginRight: 10, }, loadMore: { + flexDirection: 'row', + justifyContent: 'space-between', borderTopWidth: 1, - paddingLeft: 28, + paddingLeft: 80, + paddingRight: 20, paddingVertical: 10, + marginBottom: 8, }, })