diff options
author | dan <dan.abramov@gmail.com> | 2024-12-10 20:57:53 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-10 20:57:53 +0000 |
commit | d00879e628145ec6ded18048be212d09c0227ba8 (patch) | |
tree | 1a3b32f7640cc65f792b6162c12278c769cd504f /src/view/com/posts/FeedItem.tsx | |
parent | e052f5e198603246cb031e00d9cadc2ae4bb140d (diff) | |
download | voidsky-d00879e628145ec6ded18048be212d09c0227ba8.tar.zst |
Disambiguate feed component naming (#7040)
* Rename posts/Feed* -> posts/PostFeed* * Rename notifications/Feed* -> notifications/NotificationFeed*
Diffstat (limited to 'src/view/com/posts/FeedItem.tsx')
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 653 |
1 files changed, 0 insertions, 653 deletions
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx deleted file mode 100644 index c04921c68..000000000 --- a/src/view/com/posts/FeedItem.tsx +++ /dev/null @@ -1,653 +0,0 @@ -import React, {memo, useMemo, useState} from 'react' -import {StyleSheet, View} from 'react-native' -import { - AppBskyActorDefs, - AppBskyFeedDefs, - AppBskyFeedPost, - AppBskyFeedThreadgate, - AtUri, - ModerationDecision, - RichText as RichTextAPI, -} from '@atproto/api' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useQueryClient} from '@tanstack/react-query' - -import {isReasonFeedSource, ReasonFeedSource} from '#/lib/api/feed/types' -import {MAX_POST_LINES} from '#/lib/constants' -import {usePalette} from '#/lib/hooks/usePalette' -import {makeProfileLink} from '#/lib/routes/links' -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {sanitizeHandle} from '#/lib/strings/handles' -import {countLines} from '#/lib/strings/helpers' -import {s} from '#/lib/styles' -import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' -import {useFeedFeedbackContext} from '#/state/feed-feedback' -import {precacheProfile} from '#/state/queries/profile' -import {useSession} from '#/state/session' -import {useComposerControls} from '#/state/shell/composer' -import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {FeedNameText} from '#/view/com/util/FeedInfoText' -import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' -import {PostMeta} from '#/view/com/util/PostMeta' -import {Text} from '#/view/com/util/text/Text' -import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a} from '#/alf' -import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' -import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' -import {ContentHider} from '#/components/moderation/ContentHider' -import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' -import {PostAlerts} from '#/components/moderation/PostAlerts' -import {AppModerationCause} from '#/components/Pills' -import {ProfileHoverCard} from '#/components/ProfileHoverCard' -import {RichText} from '#/components/RichText' -import {SubtleWebHover} from '#/components/SubtleWebHover' -import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' -import {AviFollowButton} from './AviFollowButton' - -interface FeedItemProps { - record: AppBskyFeedPost.Record - reason: - | AppBskyFeedDefs.ReasonRepost - | AppBskyFeedDefs.ReasonPin - | ReasonFeedSource - | {[k: string]: unknown; $type: string} - | undefined - moderation: ModerationDecision - parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined - showReplyTo: boolean - isThreadChild?: boolean - isThreadLastChild?: boolean - isThreadParent?: boolean - feedContext: string | undefined - hideTopBorder?: boolean - isParentBlocked?: boolean - isParentNotFound?: boolean -} - -export function FeedItem({ - post, - record, - reason, - feedContext, - moderation, - parentAuthor, - showReplyTo, - isThreadChild, - isThreadLastChild, - isThreadParent, - hideTopBorder, - isParentBlocked, - isParentNotFound, - rootPost, -}: FeedItemProps & { - post: AppBskyFeedDefs.PostView - rootPost: AppBskyFeedDefs.PostView -}): React.ReactNode { - const postShadowed = usePostShadow(post) - const richText = useMemo( - () => - new RichTextAPI({ - text: record.text, - facets: record.facets, - }), - [record], - ) - if (postShadowed === POST_TOMBSTONE) { - return null - } - if (richText && moderation) { - return ( - <FeedItemInner - // Safeguard from clobbering per-post state below: - key={postShadowed.uri} - post={postShadowed} - record={record} - reason={reason} - feedContext={feedContext} - richText={richText} - parentAuthor={parentAuthor} - showReplyTo={showReplyTo} - moderation={moderation} - isThreadChild={isThreadChild} - isThreadLastChild={isThreadLastChild} - isThreadParent={isThreadParent} - hideTopBorder={hideTopBorder} - isParentBlocked={isParentBlocked} - isParentNotFound={isParentNotFound} - rootPost={rootPost} - /> - ) - } - return null -} - -let FeedItemInner = ({ - post, - record, - reason, - feedContext, - richText, - moderation, - parentAuthor, - showReplyTo, - isThreadChild, - isThreadLastChild, - isThreadParent, - hideTopBorder, - isParentBlocked, - isParentNotFound, - rootPost, -}: FeedItemProps & { - richText: RichTextAPI - post: Shadow<AppBskyFeedDefs.PostView> - rootPost: AppBskyFeedDefs.PostView -}): React.ReactNode => { - const queryClient = useQueryClient() - const {openComposer} = useComposerControls() - const pal = usePalette('default') - const {_} = useLingui() - - const href = useMemo(() => { - const urip = new AtUri(post.uri) - return makeProfileLink(post.author, 'post', urip.rkey) - }, [post.uri, post.author]) - const {sendInteraction} = useFeedFeedbackContext() - - const onPressReply = React.useCallback(() => { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionReply', - feedContext, - }) - openComposer({ - replyTo: { - uri: post.uri, - cid: post.cid, - text: record.text || '', - author: post.author, - embed: post.embed, - moderation, - }, - }) - }, [post, record, openComposer, moderation, sendInteraction, feedContext]) - - const onOpenAuthor = React.useCallback(() => { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#clickthroughAuthor', - feedContext, - }) - }, [sendInteraction, post, feedContext]) - - const onOpenReposter = React.useCallback(() => { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#clickthroughReposter', - feedContext, - }) - }, [sendInteraction, post, feedContext]) - - const onOpenEmbed = React.useCallback(() => { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#clickthroughEmbed', - feedContext, - }) - }, [sendInteraction, post, feedContext]) - - const onBeforePress = React.useCallback(() => { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#clickthroughItem', - feedContext, - }) - precacheProfile(queryClient, post.author) - }, [queryClient, post, sendInteraction, feedContext]) - - const outerStyles = [ - styles.outer, - { - borderColor: pal.colors.border, - paddingBottom: - isThreadLastChild || (!isThreadChild && !isThreadParent) - ? 8 - : undefined, - borderTopWidth: - hideTopBorder || isThreadChild ? 0 : StyleSheet.hairlineWidth, - }, - ] - - const {currentAccount} = useSession() - const isOwner = - AppBskyFeedDefs.isReasonRepost(reason) && - reason.by.did === currentAccount?.did - - /** - * If `post[0]` in this slice is the actual root post (not an orphan thread), - * then we may have a threadgate record to reference - */ - const threadgateRecord = AppBskyFeedThreadgate.isRecord( - rootPost.threadgate?.record, - ) - ? rootPost.threadgate.record - : undefined - - const [hover, setHover] = useState(false) - return ( - <Link - testID={`feedItem-by-${post.author.handle}`} - style={outerStyles} - href={href} - noFeedback - accessible={false} - onBeforePress={onBeforePress} - dataSet={{feedContext}} - onPointerEnter={() => { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }}> - <SubtleWebHover hover={hover} /> - <View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}> - <View style={{width: 42}}> - {isThreadChild && ( - <View - style={[ - styles.replyLine, - { - flexGrow: 1, - backgroundColor: pal.colors.replyLine, - marginBottom: 4, - }, - ]} - /> - )} - </View> - - <View style={{paddingTop: 12, flexShrink: 1}}> - {isReasonFeedSource(reason) ? ( - <Link href={reason.href}> - <Text - type="sm-bold" - style={pal.textLight} - lineHeight={1.2} - numberOfLines={1}> - <Trans context="from-feed"> - From{' '} - <FeedNameText - type="sm-bold" - uri={reason.uri} - href={reason.href} - lineHeight={1.2} - numberOfLines={1} - style={pal.textLight} - /> - </Trans> - </Text> - </Link> - ) : AppBskyFeedDefs.isReasonRepost(reason) ? ( - <Link - style={styles.includeReason} - href={makeProfileLink(reason.by)} - title={ - isOwner - ? _(msg`Reposted by you`) - : _( - msg`Reposted by ${sanitizeDisplayName( - reason.by.displayName || reason.by.handle, - )}`, - ) - } - onBeforePress={onOpenReposter}> - <RepostIcon - style={{color: pal.colors.textLight, marginRight: 3}} - width={13} - height={13} - /> - <Text - type="sm-bold" - style={pal.textLight} - lineHeight={1.2} - numberOfLines={1}> - {isOwner ? ( - <Trans>Reposted by you</Trans> - ) : ( - <Trans> - Reposted by{' '} - <ProfileHoverCard inline did={reason.by.did}> - <TextLinkOnWebOnly - type="sm-bold" - style={pal.textLight} - lineHeight={1.2} - numberOfLines={1} - text={ - <Text - emoji - type="sm-bold" - style={pal.textLight} - lineHeight={1.2}> - {sanitizeDisplayName( - reason.by.displayName || - sanitizeHandle(reason.by.handle), - moderation.ui('displayName'), - )} - </Text> - } - href={makeProfileLink(reason.by)} - onBeforePress={onOpenReposter} - /> - </ProfileHoverCard> - </Trans> - )} - </Text> - </Link> - ) : AppBskyFeedDefs.isReasonPin(reason) ? ( - <View style={styles.includeReason}> - <PinIcon - style={{color: pal.colors.textLight, marginRight: 3}} - width={13} - height={13} - /> - <Text - type="sm-bold" - style={pal.textLight} - lineHeight={1.2} - numberOfLines={1}> - <Trans>Pinned</Trans> - </Text> - </View> - ) : null} - </View> - </View> - - <View style={styles.layout}> - <View style={styles.layoutAvi}> - <AviFollowButton author={post.author} moderation={moderation}> - <PreviewableUserAvatar - size={42} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - onBeforePress={onOpenAuthor} - /> - </AviFollowButton> - {isThreadParent && ( - <View - style={[ - styles.replyLine, - { - flexGrow: 1, - backgroundColor: pal.colors.replyLine, - marginTop: 4, - }, - ]} - /> - )} - </View> - <View style={styles.layoutContent}> - <PostMeta - author={post.author} - moderation={moderation} - timestamp={post.indexedAt} - postHref={href} - onOpenAuthor={onOpenAuthor} - /> - {showReplyTo && - (parentAuthor || isParentBlocked || isParentNotFound) && ( - <ReplyToLabel - blocked={isParentBlocked} - notFound={isParentNotFound} - profile={parentAuthor} - /> - )} - <LabelsOnMyPost post={post} /> - <PostContent - moderation={moderation} - richText={richText} - postEmbed={post.embed} - postAuthor={post.author} - onOpenEmbed={onOpenEmbed} - post={post} - threadgateRecord={threadgateRecord} - /> - <PostCtrls - post={post} - record={record} - richText={richText} - onPressReply={onPressReply} - logContext="FeedItem" - feedContext={feedContext} - threadgateRecord={threadgateRecord} - /> - </View> - </View> - </Link> - ) -} -FeedItemInner = memo(FeedItemInner) - -let PostContent = ({ - post, - moderation, - richText, - postEmbed, - postAuthor, - onOpenEmbed, - threadgateRecord, -}: { - moderation: ModerationDecision - richText: RichTextAPI - postEmbed: AppBskyFeedDefs.PostView['embed'] - postAuthor: AppBskyFeedDefs.PostView['author'] - onOpenEmbed: () => void - post: AppBskyFeedDefs.PostView - threadgateRecord?: AppBskyFeedThreadgate.Record -}): React.ReactNode => { - const pal = usePalette('default') - const {_} = useLingui() - const {currentAccount} = useSession() - const [limitLines, setLimitLines] = useState( - () => countLines(richText.text) >= MAX_POST_LINES, - ) - const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ - threadgateRecord, - }) - const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { - const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) - const rootPostUri = AppBskyFeedPost.isRecord(post.record) - ? post.record?.reply?.root?.uri || post.uri - : undefined - const isControlledByViewer = - rootPostUri && new AtUri(rootPostUri).host === currentAccount?.did - return isControlledByViewer && isPostHiddenByThreadgate - ? [ - { - type: 'reply-hidden', - source: {type: 'user', did: currentAccount?.did}, - priority: 6, - }, - ] - : [] - }, [post, currentAccount?.did, threadgateHiddenReplies]) - - const onPressShowMore = React.useCallback(() => { - setLimitLines(false) - }, [setLimitLines]) - - return ( - <ContentHider - testID="contentHider-post" - modui={moderation.ui('contentList')} - ignoreMute - childContainerStyle={styles.contentHiderChild}> - <PostAlerts - modui={moderation.ui('contentList')} - style={[a.py_2xs]} - additionalCauses={additionalPostAlerts} - /> - {richText.text ? ( - <View style={styles.postTextContainer}> - <RichText - enableTags - testID="postText" - value={richText} - numberOfLines={limitLines ? MAX_POST_LINES : undefined} - style={[a.flex_1, a.text_md]} - authorHandle={postAuthor.handle} - /> - </View> - ) : undefined} - {limitLines ? ( - <TextLink - text={_(msg`Show More`)} - style={pal.link} - onPress={onPressShowMore} - href="#" - /> - ) : undefined} - {postEmbed ? ( - <View style={[a.pb_xs]}> - <PostEmbeds - embed={postEmbed} - moderation={moderation} - onOpen={onOpenEmbed} - viewContext={PostEmbedViewContext.Feed} - /> - </View> - ) : null} - </ContentHider> - ) -} -PostContent = memo(PostContent) - -function ReplyToLabel({ - profile, - blocked, - notFound, -}: { - profile: AppBskyActorDefs.ProfileViewBasic | undefined - blocked?: boolean - notFound?: boolean -}) { - const pal = usePalette('default') - const {currentAccount} = useSession() - - let label - if (blocked) { - label = <Trans context="description">Reply to a blocked post</Trans> - } else if (notFound) { - label = <Trans context="description">Reply to a post</Trans> - } else if (profile != null) { - const isMe = profile.did === currentAccount?.did - if (isMe) { - label = <Trans context="description">Reply to you</Trans> - } else { - label = ( - <Trans context="description"> - Reply to{' '} - <ProfileHoverCard inline did={profile.did}> - <TextLinkOnWebOnly - type="md" - style={pal.textLight} - lineHeight={1.2} - numberOfLines={1} - href={makeProfileLink(profile)} - text={ - <Text emoji type="md" style={pal.textLight} lineHeight={1.2}> - {profile.displayName - ? sanitizeDisplayName(profile.displayName) - : sanitizeHandle(profile.handle)} - </Text> - } - /> - </ProfileHoverCard> - </Trans> - ) - } - } - - if (!label) { - // Should not happen. - return null - } - - return ( - <View style={[s.flexRow, s.mb2, s.alignCenter]}> - <FontAwesomeIcon - icon="reply" - size={9} - style={[{color: pal.colors.textLight} as FontAwesomeIconStyle, s.mr5]} - /> - <Text - type="md" - style={[pal.textLight, s.mr2]} - lineHeight={1.2} - numberOfLines={1}> - {label} - </Text> - </View> - ) -} - -const styles = StyleSheet.create({ - outer: { - paddingLeft: 10, - paddingRight: 15, - // @ts-ignore web only -prf - cursor: 'pointer', - }, - replyLine: { - width: 2, - marginLeft: 'auto', - marginRight: 'auto', - }, - includeReason: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 2, - marginBottom: 2, - marginLeft: -16, - }, - layout: { - flexDirection: 'row', - marginTop: 1, - }, - layoutAvi: { - paddingLeft: 8, - paddingRight: 10, - position: 'relative', - zIndex: 999, - }, - layoutContent: { - position: 'relative', - flex: 1, - zIndex: 0, - }, - alert: { - marginTop: 6, - marginBottom: 6, - }, - postTextContainer: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - paddingBottom: 2, - overflow: 'hidden', - }, - contentHiderChild: { - marginTop: 6, - }, - embed: { - marginBottom: 6, - }, - translateLink: { - marginBottom: 6, - }, -}) |