diff options
Diffstat (limited to 'src/view/com/post-thread/PostThreadItem.tsx')
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 463 |
1 files changed, 231 insertions, 232 deletions
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 351a46706..a4b7a4a9c 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 React, {memo, useMemo} from 'react' +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' @@ -21,10 +20,10 @@ import {sanitizeHandle} from 'lib/strings/handles' import {countLines, pluralize} from 'lib/strings/helpers' import {isEmbedByEmbedder} from 'lib/embeds' 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 {PostDropdownBtn} from '../util/forms/PostDropdownBtn' import {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' @@ -36,125 +35,172 @@ 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, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useLanguagePrefs} from '#/state/preferences' +import {useComposerControls} from '#/state/shell/composer' +import {useModerationOpts} from '#/state/queries/preferences' +import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' -export const PostThreadItem = observer(function PostThreadItem({ - item, - onPostReply, - hasPrecedingItem, +export function PostThreadItem({ + post, + record, treeView, + depth, + isHighlightedPost, + hasMore, + showChildReplyLine, + showParentReplyLine, + hasPrecedingItem, + onPostReply, }: { - item: PostThreadItemModel - onPostReply: () => void - hasPrecedingItem: boolean + post: AppBskyFeedDefs.PostView + record: AppBskyFeedPost.Record treeView: boolean + depth: number + isHighlightedPost?: boolean + hasMore?: boolean + showChildReplyLine?: boolean + showParentReplyLine?: boolean + hasPrecedingItem: boolean + onPostReply: () => void }) { + const moderationOpts = useModerationOpts() + const postShadowed = usePostShadow(post) + const richText = useMemo( + () => + new RichTextAPI({ + text: record.text, + facets: record.facets, + }), + [record], + ) + const moderation = useMemo( + () => + post && moderationOpts ? moderatePost(post, moderationOpts) : undefined, + [post, moderationOpts], + ) + if (postShadowed === POST_TOMBSTONE) { + return <PostThreadItemDeleted /> + } + if (richText && moderation) { + return ( + <PostThreadItemLoaded + post={postShadowed} + record={record} + richText={richText} + moderation={moderation} + treeView={treeView} + depth={depth} + isHighlightedPost={isHighlightedPost} + hasMore={hasMore} + showChildReplyLine={showChildReplyLine} + showParentReplyLine={showParentReplyLine} + hasPrecedingItem={hasPrecedingItem} + onPostReply={onPostReply} + /> + ) + } + return null +} + +function PostThreadItemDeleted() { + const styles = useStyles() + const pal = usePalette('default') + return ( + <View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}> + <FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} /> + <Text style={[pal.textLight, s.ml10]}> + <Trans>This post has been deleted.</Trans> + </Text> + </View> + ) +} + +let PostThreadItemLoaded = ({ + post, + record, + richText, + moderation, + treeView, + depth, + isHighlightedPost, + hasMore, + showChildReplyLine, + showParentReplyLine, + hasPrecedingItem, + onPostReply, +}: { + post: Shadow<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 +}): React.ReactNode => { const pal = usePalette('default') - const store = useStores() - const [deleted, setDeleted] = React.useState(false) + const langPrefs = useLanguagePrefs() + const {openComposer} = useComposerControls() 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( record?.text || '', - store.preferences.primaryLanguage, + langPrefs.primaryLanguage, ) const needsTranslation = useMemo( () => Boolean( - store.preferences.primaryLanguage && - !isPostInLanguage(item.post, [store.preferences.primaryLanguage]), + langPrefs.primaryLanguage && + !isPostInLanguage(post, [langPrefs.primaryLanguage]), ), - [item.post, store.preferences.primaryLanguage], + [post, langPrefs.primaryLanguage], ) const onPressReply = React.useCallback(() => { - store.shell.openComposer({ + 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(async () => { - try { - await item.toggleThreadMute() - if (item.isThreadMuted) { - 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]) - - 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]) + }, [openComposer, post, record, onPostReply]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) @@ -164,22 +210,10 @@ export const PostThreadItem = observer(function PostThreadItem({ return <ErrorMessage message="Invalid or unsupported post record" /> } - if (deleted) { - return ( - <View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}> - <FontAwesomeIcon - icon={['far', 'trash-can']} - style={pal.icon as FontAwesomeIconStyle} - /> - <Text style={[pal.textLight, s.ml10]}>This post has been deleted.</Text> - </View> - ) - } - - if (item._isHighlightedPost) { + if (isHighlightedPost) { return ( <> - {item.rootUri !== item.uri && ( + {rootUri !== post.uri && ( <View style={{paddingLeft: 16, flexDirection: 'row', height: 16}}> <View style={{width: 38}}> <View @@ -196,7 +230,7 @@ export const PostThreadItem = observer(function PostThreadItem({ )} <Link - testID={`postThreadItem-by-${item.post.author.handle}`} + testID={`postThreadItem-by-${post.author.handle}`} style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} noFeedback accessible={false}> @@ -205,10 +239,10 @@ export const PostThreadItem = observer(function PostThreadItem({ <View style={[styles.layoutAvi, {paddingBottom: 8}]}> <PreviewableUserAvatar size={52} - did={item.post.author.did} - handle={item.post.author.handle} - avatar={item.post.author.avatar} - moderation={item.moderation.avatar} + did={post.author.did} + handle={post.author.handle} + avatar={post.author.avatar} + moderation={moderation.avatar} /> </View> <View style={styles.layoutContent}> @@ -225,17 +259,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), )} </Text> </Link> - <TimeElapsed timestamp={item.post.indexedAt}> + <TimeElapsed timestamp={post.indexedAt}> {({timeElapsed}) => ( <Text type="md" style={[styles.metaItem, pal.textLight]} - title={niceDate(item.post.indexedAt)}> + title={niceDate(post.indexedAt)}> · {timeElapsed} </Text> )} @@ -272,23 +306,15 @@ export const PostThreadItem = observer(function PostThreadItem({ href={authorHref} title={authorTitle}> <Text type="md" style={[pal.textLight]} numberOfLines={1}> - {sanitizeHandle(item.post.author.handle, '@')} + {sanitizeHandle(post.author.handle, '@')} </Text> </Link> </View> </View> <PostDropdownBtn testID="postDropdownBtn" - itemUri={itemUri} - itemCid={itemCid} - itemHref={itemHref} - itemTitle={itemTitle} - isAuthor={item.post.author.did === store.me.did} - isThreadMuted={item.isThreadMuted} - onCopyPostText={onCopyPostText} - onOpenTranslate={onOpenTranslate} - onToggleThreadMute={onToggleThreadMute} - onDeletePost={onDeletePost} + post={post} + record={record} style={{ paddingVertical: 6, paddingHorizontal: 10, @@ -299,16 +325,16 @@ export const PostThreadItem = observer(function PostThreadItem({ </View> <View style={[s.pl10, s.pr10, s.pb10]}> <ContentHider - moderation={item.moderation.content} + moderation={moderation.content} ignoreMute style={styles.contentHider} childContainerStyle={styles.contentHiderChild}> <PostAlerts - moderation={item.moderation.content} + moderation={moderation.content} includeMute style={styles.alert} /> - {item.richText?.text ? ( + {richText?.text ? ( <View style={[ styles.postTextContainer, @@ -316,59 +342,56 @@ export const PostThreadItem = observer(function PostThreadItem({ ]}> <RichText type="post-text-lg" - richText={item.richText} + richText={richText} lineHeight={1.3} style={s.flex1} /> </View> ) : undefined} - {item.post.embed && ( + {post.embed && ( <ContentHider - moderation={item.moderation.embed} - ignoreMute={isEmbedByEmbedder( - item.post.embed, - item.post.author.did, - )} + moderation={moderation.embed} + ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} style={s.mb10}> <PostEmbeds - embed={item.post.embed} - moderation={item.moderation.embed} + embed={post.embed} + moderation={moderation.embed} /> </ContentHider> )} </ContentHider> <ExpandedPostDetails - post={item.post} + post={post} translatorUrl={translatorUrl} needsTranslation={needsTranslation} /> {hasEngagement ? ( <View style={[styles.expandedInfo, pal.border]}> - {item.post.repostCount ? ( + {post.repostCount ? ( <Link style={styles.expandedInfoItem} href={repostsHref} title={repostsTitle}> <Text testID="repostCount" type="lg" style={pal.textLight}> <Text type="xl-bold" style={pal.text}> - {formatCount(item.post.repostCount)} + {formatCount(post.repostCount)} </Text>{' '} - {pluralize(item.post.repostCount, 'repost')} + {pluralize(post.repostCount, 'repost')} </Text> </Link> ) : ( <></> )} - {item.post.likeCount ? ( + {post.likeCount ? ( <Link style={styles.expandedInfoItem} href={likesHref} title={likesTitle}> <Text testID="likeCount" type="lg" style={pal.textLight}> <Text type="xl-bold" style={pal.text}> - {formatCount(item.post.likeCount)} + {formatCount(post.likeCount)} </Text>{' '} - {pluralize(item.post.likeCount, 'like')} + {pluralize(post.likeCount, 'like')} </Text> </Link> ) : ( @@ -381,24 +404,9 @@ export const PostThreadItem = observer(function PostThreadItem({ <View style={[s.pl10, s.pb5]}> <PostCtrls big - itemUri={itemUri} - itemCid={itemCid} - itemHref={itemHref} - itemTitle={itemTitle} - author={item.post.author} - text={item.richText?.text || record.text} - indexedAt={item.post.indexedAt} - isAuthor={item.post.author.did === store.me.did} - isReposted={!!item.post.viewer?.repost} - isLiked={!!item.post.viewer?.like} - isThreadMuted={item.isThreadMuted} + post={post} + record={record} onPressReply={onPressReply} - onPressToggleRepost={onPressToggleRepost} - onPressToggleLike={onPressToggleLike} - onCopyPostText={onCopyPostText} - onOpenTranslate={onOpenTranslate} - onToggleThreadMute={onToggleThreadMute} - onDeletePost={onDeletePost} /> </View> </View> @@ -406,17 +414,19 @@ export const PostThreadItem = observer(function PostThreadItem({ </> ) } else { - const isThreadedChild = treeView && item._depth > 1 + const isThreadedChild = treeView && depth > 1 return ( <PostOuterWrapper - item={item} - hasPrecedingItem={hasPrecedingItem} - treeView={treeView}> + post={post} + depth={depth} + showParentReplyLine={!!showParentReplyLine} + treeView={treeView} + hasPrecedingItem={hasPrecedingItem}> <PostHider - testID={`postThreadItem-by-${item.post.author.handle}`} - href={itemHref} + testID={`postThreadItem-by-${post.author.handle}`} + href={postHref} style={[pal.view]} - moderation={item.moderation.content}> + moderation={moderation.content}> <PostSandboxWarning /> <View @@ -427,7 +437,7 @@ export const PostThreadItem = observer(function PostThreadItem({ height: isThreadedChild ? 8 : 16, }}> <View style={{width: 38}}> - {!isThreadedChild && item._showParentReplyLine && ( + {!isThreadedChild && showParentReplyLine && ( <View style={[ styles.replyLine, @@ -446,21 +456,20 @@ export const PostThreadItem = observer(function PostThreadItem({ style={[ styles.layout, { - paddingBottom: - item._showChildReplyLine && !isThreadedChild ? 0 : 8, + paddingBottom: showChildReplyLine && !isThreadedChild ? 0 : 8, }, ]}> {!isThreadedChild && ( <View style={styles.layoutAvi}> <PreviewableUserAvatar size={38} - did={item.post.author.did} - handle={item.post.author.handle} - avatar={item.post.author.avatar} - moderation={item.moderation.avatar} + did={post.author.did} + handle={post.author.handle} + avatar={post.author.avatar} + moderation={moderation.avatar} /> - {item._showChildReplyLine && ( + {showChildReplyLine && ( <View style={[ styles.replyLine, @@ -477,10 +486,10 @@ export const PostThreadItem = observer(function PostThreadItem({ <View style={styles.layoutContent}> <PostMeta - author={item.post.author} - authorHasWarning={!!item.post.author.labels?.length} - timestamp={item.post.indexedAt} - postHref={itemHref} + author={post.author} + authorHasWarning={!!post.author.labels?.length} + timestamp={post.indexedAt} + postHref={postHref} showAvatar={isThreadedChild} avatarSize={26} displayNameType="md-bold" @@ -488,14 +497,14 @@ export const PostThreadItem = observer(function PostThreadItem({ style={isThreadedChild && s.mb5} /> <PostAlerts - moderation={item.moderation.content} + moderation={moderation.content} style={styles.alert} /> - {item.richText?.text ? ( + {richText?.text ? ( <View style={styles.postTextContainer}> <RichText type="post-text" - richText={item.richText} + richText={richText} style={[pal.text, s.flex1]} lineHeight={1.3} numberOfLines={limitLines ? MAX_POST_LINES : undefined} @@ -510,42 +519,24 @@ export const PostThreadItem = observer(function PostThreadItem({ href="#" /> ) : undefined} - {item.post.embed && ( + {post.embed && ( <ContentHider style={styles.contentHider} - moderation={item.moderation.embed}> + moderation={moderation.embed}> <PostEmbeds - embed={item.post.embed} - moderation={item.moderation.embed} + embed={post.embed} + moderation={moderation.embed} /> </ContentHider> )} <PostCtrls - itemUri={itemUri} - itemCid={itemCid} - itemHref={itemHref} - itemTitle={itemTitle} - author={item.post.author} - text={item.richText?.text || record.text} - indexedAt={item.post.indexedAt} - isAuthor={item.post.author.did === store.me.did} - replyCount={item.post.replyCount} - repostCount={item.post.repostCount} - likeCount={item.post.likeCount} - isReposted={!!item.post.viewer?.repost} - isLiked={!!item.post.viewer?.like} - isThreadMuted={item.isThreadMuted} + post={post} + record={record} onPressReply={onPressReply} - onPressToggleRepost={onPressToggleRepost} - onPressToggleLike={onPressToggleLike} - onCopyPostText={onCopyPostText} - onOpenTranslate={onOpenTranslate} - onToggleThreadMute={onToggleThreadMute} - onDeletePost={onDeletePost} /> </View> </View> - {item._hasMore ? ( + {hasMore ? ( <Link style={[ styles.loadMore, @@ -555,7 +546,7 @@ export const PostThreadItem = observer(function PostThreadItem({ paddingBottom: treeView ? 4 : 12, }, ]} - href={itemHref} + href={postHref} title={itemTitle} noFeedback> <Text type="sm-medium" style={pal.textLight}> @@ -572,22 +563,27 @@ export const PostThreadItem = observer(function PostThreadItem({ </PostOuterWrapper> ) } -}) +} +PostThreadItemLoaded = memo(PostThreadItemLoaded) 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 ( <View style={[ @@ -597,13 +593,13 @@ function PostOuterWrapper({ { flexDirection: 'row', paddingLeft: 20, - borderTopWidth: item._depth === 1 ? 1 : 0, - paddingTop: item._depth === 1 ? 8 : 0, + borderTopWidth: depth === 1 ? 1 : 0, + paddingTop: depth === 1 ? 8 : 0, }, ]}> - {Array.from(Array(item._depth - 1)).map((_, n: number) => ( + {Array.from(Array(depth - 1)).map((_, n: number) => ( <View - key={`${item.uri}-padding-${n}`} + key={`${post.uri}-padding-${n}`} style={{ borderLeftWidth: 2, borderLeftColor: pal.colors.border, @@ -622,7 +618,7 @@ function PostOuterWrapper({ styles.outer, pal.view, pal.border, - item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder, + showParentReplyLine && hasPrecedingItem && styles.noTopBorder, styles.cursor, ]}> {children} @@ -640,14 +636,17 @@ function ExpandedPostDetails({ translatorUrl: string }) { const pal = usePalette('default') + const {_} = useLingui() return ( <View style={[s.flexRow, s.mt2, s.mb10]}> <Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text> {needsTranslation && ( <> - <Text style={pal.textLight}> • </Text> - <Link href={translatorUrl} title="Translate"> - <Text style={pal.link}>Translate</Text> + <Text style={[pal.textLight, s.ml5, s.mr5]}>•</Text> + <Link href={translatorUrl} title={_(msg`Translate`)}> + <Text style={pal.link}> + <Trans>Translate</Trans> + </Text> </Link> </> )} |