From fb4f5709c43c070653c917e3196b9b1c120418a6 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 9 Nov 2023 15:35:25 -0800 Subject: Refactor post threads to use react query (#1851) * Add post and post-thread queries * Update PostThread components to use new queries * Move from normalized cache to shadow cache model * Merge post shadow into the post automatically * Remove dead code * Remove old temporary session * Fix: set agent on session creation * Temporarily double-login * Handle post-thread uri resolution errors --- src/view/com/post-thread/PostThreadItem.tsx | 447 ++++++++++++++-------------- 1 file changed, 219 insertions(+), 228 deletions(-) (limited to 'src/view/com/post-thread/PostThreadItem.tsx') diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 49b769e13..a8e0c0f93 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,18 +1,17 @@ import React, {useMemo} from 'react' -import {observer} from 'mobx-react-lite' -import {Linking, StyleSheet, View} from 'react-native' -import Clipboard from '@react-native-clipboard/clipboard' -import {AtUri, AppBskyFeedDefs} from '@atproto/api' +import {StyleSheet, View} from 'react-native' import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {PostThreadItemModel} from 'state/models/content/post-thread-item' + AtUri, + AppBskyFeedDefs, + AppBskyFeedPost, + RichText as RichTextAPI, + moderatePost, + PostModeration, +} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Link, TextLink} from '../util/Link' import {RichText} from '../util/text/RichText' import {Text} from '../util/text/Text' -import {PostDropdownBtn} from '../util/forms/PostDropdownBtn' -import * as Toast from '../util/Toast' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {niceDate} from 'lib/strings/time' @@ -24,7 +23,8 @@ import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {useStores} from 'state/index' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/post-embeds' -import {PostCtrls} from '../util/post-ctrls/PostCtrls' +import {PostCtrls} from '../util/post-ctrls/PostCtrls2' +import {PostDropdownBtn} from '../util/forms/PostDropdownBtn2' import {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' @@ -36,54 +36,145 @@ import {TimeElapsed} from 'view/com/util/TimeElapsed' import {makeProfileLink} from 'lib/routes/links' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {MAX_POST_LINES} from 'lib/constants' -import {logger} from '#/logger' import {Trans} from '@lingui/macro' -import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' +import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' -export const PostThreadItem = observer(function PostThreadItem({ - item, - onPostReply, - hasPrecedingItem, +export function PostThreadItem({ + post, + record, + dataUpdatedAt, treeView, + depth, + isHighlightedPost, + hasMore, + showChildReplyLine, + showParentReplyLine, + hasPrecedingItem, + onPostReply, }: { - item: PostThreadItemModel - onPostReply: () => void + post: AppBskyFeedDefs.PostView + record: AppBskyFeedPost.Record + dataUpdatedAt: number + treeView: boolean + depth: number + isHighlightedPost?: boolean + hasMore?: boolean + showChildReplyLine?: boolean + showParentReplyLine?: boolean hasPrecedingItem: boolean + onPostReply: () => void +}) { + const store = useStores() + const postShadowed = usePostShadow(post, dataUpdatedAt) + const richText = useMemo( + () => + post && + AppBskyFeedPost.isRecord(post?.record) && + AppBskyFeedPost.validateRecord(post?.record).success + ? new RichTextAPI({ + text: post.record.text, + facets: post.record.facets, + }) + : undefined, + [post], + ) + const moderation = useMemo( + () => + post ? moderatePost(post, store.preferences.moderationOpts) : undefined, + [post, store], + ) + if (postShadowed === POST_TOMBSTONE) { + return + } + if (richText && moderation) { + return ( + + ) + } + return null +} + +function PostThreadItemDeleted() { + const styles = useStyles() + const pal = usePalette('default') + return ( + + + + This post has been deleted. + + + ) +} + +function PostThreadItemLoaded({ + post, + record, + richText, + moderation, + treeView, + depth, + isHighlightedPost, + hasMore, + showChildReplyLine, + showParentReplyLine, + hasPrecedingItem, + onPostReply, +}: { + post: AppBskyFeedDefs.PostView + record: AppBskyFeedPost.Record + richText: RichTextAPI + moderation: PostModeration treeView: boolean + depth: number + isHighlightedPost?: boolean + hasMore?: boolean + showChildReplyLine?: boolean + showParentReplyLine?: boolean + hasPrecedingItem: boolean + onPostReply: () => void }) { const pal = usePalette('default') const store = useStores() - const mutedThreads = useMutedThreads() - const toggleThreadMute = useToggleThreadMute() const langPrefs = useLanguagePrefs() - const [deleted, setDeleted] = React.useState(false) const [limitLines, setLimitLines] = React.useState( - countLines(item.richText?.text) >= MAX_POST_LINES, + countLines(richText?.text) >= MAX_POST_LINES, ) const styles = useStyles() - const record = item.postRecord - const hasEngagement = item.post.likeCount || item.post.repostCount + const hasEngagement = post.likeCount || post.repostCount - const itemUri = item.post.uri - const itemCid = item.post.cid - const itemHref = React.useMemo(() => { - const urip = new AtUri(item.post.uri) - return makeProfileLink(item.post.author, 'post', urip.rkey) - }, [item.post.uri, item.post.author]) - const itemTitle = `Post by ${item.post.author.handle}` - const authorHref = makeProfileLink(item.post.author) - const authorTitle = item.post.author.handle - const isAuthorMuted = item.post.author.viewer?.muted + const rootUri = record.reply?.root?.uri || post.uri + const postHref = React.useMemo(() => { + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey) + }, [post.uri, post.author]) + const itemTitle = `Post by ${post.author.handle}` + const authorHref = makeProfileLink(post.author) + const authorTitle = post.author.handle + const isAuthorMuted = post.author.viewer?.muted const likesHref = React.useMemo(() => { - const urip = new AtUri(item.post.uri) - return makeProfileLink(item.post.author, 'post', urip.rkey, 'liked-by') - }, [item.post.uri, item.post.author]) + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') + }, [post.uri, post.author]) const likesTitle = 'Likes on this post' const repostsHref = React.useMemo(() => { - const urip = new AtUri(item.post.uri) - return makeProfileLink(item.post.author, 'post', urip.rkey, 'reposted-by') - }, [item.post.uri, item.post.author]) + const urip = new AtUri(post.uri) + return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') + }, [post.uri, post.author]) const repostsTitle = 'Reposts of this post' const translatorUrl = getTranslatorLink( @@ -94,73 +185,26 @@ export const PostThreadItem = observer(function PostThreadItem({ () => Boolean( langPrefs.primaryLanguage && - !isPostInLanguage(item.post, [langPrefs.primaryLanguage]), + !isPostInLanguage(post, [langPrefs.primaryLanguage]), ), - [item.post, langPrefs.primaryLanguage], + [post, langPrefs.primaryLanguage], ) const onPressReply = React.useCallback(() => { store.shell.openComposer({ replyTo: { - uri: item.post.uri, - cid: item.post.cid, - text: record?.text as string, + uri: post.uri, + cid: post.cid, + text: record.text, author: { - handle: item.post.author.handle, - displayName: item.post.author.displayName, - avatar: item.post.author.avatar, + handle: post.author.handle, + displayName: post.author.displayName, + avatar: post.author.avatar, }, }, onPost: onPostReply, }) - }, [store, item, record, onPostReply]) - - const onPressToggleRepost = React.useCallback(() => { - return item - .toggleRepost() - .catch(e => logger.error('Failed to toggle repost', {error: e})) - }, [item]) - - const onPressToggleLike = React.useCallback(() => { - return item - .toggleLike() - .catch(e => logger.error('Failed to toggle like', {error: e})) - }, [item]) - - const onCopyPostText = React.useCallback(() => { - Clipboard.setString(record?.text || '') - Toast.show('Copied to clipboard') - }, [record]) - - const onOpenTranslate = React.useCallback(() => { - Linking.openURL(translatorUrl) - }, [translatorUrl]) - - const onToggleThreadMute = React.useCallback(() => { - try { - const muted = toggleThreadMute(item.data.rootUri) - if (muted) { - Toast.show('You will no longer receive notifications for this thread') - } else { - Toast.show('You will now receive notifications for this thread') - } - } catch (e) { - logger.error('Failed to toggle thread mute', {error: e}) - } - }, [item, toggleThreadMute]) - - const onDeletePost = React.useCallback(() => { - item.delete().then( - () => { - setDeleted(true) - Toast.show('Post deleted') - }, - e => { - logger.error('Failed to delete post', {error: e}) - Toast.show('Failed to delete post, please try again') - }, - ) - }, [item]) + }, [store, post, record, onPostReply]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) @@ -170,24 +214,10 @@ export const PostThreadItem = observer(function PostThreadItem({ return } - if (deleted) { - return ( - - - - This post has been deleted. - - - ) - } - - if (item._isHighlightedPost) { + if (isHighlightedPost) { return ( <> - {item.rootUri !== item.uri && ( + {rootUri !== post.uri && ( @@ -213,10 +243,10 @@ export const PostThreadItem = observer(function PostThreadItem({ @@ -233,17 +263,17 @@ export const PostThreadItem = observer(function PostThreadItem({ numberOfLines={1} lineHeight={1.2}> {sanitizeDisplayName( - item.post.author.displayName || - sanitizeHandle(item.post.author.handle), + post.author.displayName || + sanitizeHandle(post.author.handle), )} - + {({timeElapsed}) => ( + title={niceDate(post.indexedAt)}> · {timeElapsed} )} @@ -280,23 +310,15 @@ export const PostThreadItem = observer(function PostThreadItem({ href={authorHref} title={authorTitle}> - {sanitizeHandle(item.post.author.handle, '@')} + {sanitizeHandle(post.author.handle, '@')} - {item.richText?.text ? ( + {richText?.text ? ( ) : undefined} - {item.post.embed && ( + {post.embed && ( )} {hasEngagement ? ( - {item.post.repostCount ? ( + {post.repostCount ? ( - {formatCount(item.post.repostCount)} + {formatCount(post.repostCount)} {' '} - {pluralize(item.post.repostCount, 'repost')} + {pluralize(post.repostCount, 'repost')} ) : ( <> )} - {item.post.likeCount ? ( + {post.likeCount ? ( - {formatCount(item.post.likeCount)} + {formatCount(post.likeCount)} {' '} - {pluralize(item.post.likeCount, 'like')} + {pluralize(post.likeCount, 'like')} ) : ( @@ -389,24 +408,9 @@ export const PostThreadItem = observer(function PostThreadItem({ @@ -414,17 +418,19 @@ export const PostThreadItem = observer(function PostThreadItem({ ) } else { - const isThreadedChild = treeView && item._depth > 1 + const isThreadedChild = treeView && depth > 1 return ( + post={post} + depth={depth} + showParentReplyLine={!!showParentReplyLine} + treeView={treeView} + hasPrecedingItem={hasPrecedingItem}> + moderation={moderation.content}> - {!isThreadedChild && item._showParentReplyLine && ( + {!isThreadedChild && showParentReplyLine && ( {!isThreadedChild && ( - {item._showChildReplyLine && ( + {showChildReplyLine && ( - {item.richText?.text ? ( + {richText?.text ? ( ) : undefined} - {item.post.embed && ( + {post.embed && ( + moderation={moderation.embed}> )} - {item._hasMore ? ( + {hasMore ? ( @@ -580,22 +567,26 @@ export const PostThreadItem = observer(function PostThreadItem({ ) } -}) +} function PostOuterWrapper({ - item, - hasPrecedingItem, + post, treeView, + depth, + showParentReplyLine, + hasPrecedingItem, children, }: React.PropsWithChildren<{ - item: PostThreadItemModel - hasPrecedingItem: boolean + post: AppBskyFeedDefs.PostView treeView: boolean + depth: number + showParentReplyLine: boolean + hasPrecedingItem: boolean }>) { const {isMobile} = useWebMediaQueries() const pal = usePalette('default') const styles = useStyles() - if (treeView && item._depth > 1) { + if (treeView && depth > 1) { return ( - {Array.from(Array(item._depth - 1)).map((_, n: number) => ( + {Array.from(Array(depth - 1)).map((_, n: number) => ( {children} -- cgit 1.4.1