import React, {memo, useMemo, useState} from 'react' import {StyleSheet, View} from 'react-native' import { AppBskyActorDefs, AppBskyFeedDefs, AppBskyFeedPost, 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 {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useComposerControls} from '#/state/shell/composer' 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 {precacheProfile} from 'state/queries/profile' import {atoms as a} from '#/alf' import {ContentHider} from '#/components/moderation/ContentHider' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' import {FeedNameText} from '../util/FeedInfoText' import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostEmbeds} from '../util/post-embeds' import {PostMeta} from '../util/PostMeta' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {AviFollowButton} from './AviFollowButton' import hairlineWidth = StyleSheet.hairlineWidth import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' interface FeedItemProps { record: AppBskyFeedPost.Record reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined moderation: ModerationDecision parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined showReplyTo: boolean isThreadChild?: boolean isThreadLastChild?: boolean isThreadParent?: boolean feedContext: string | undefined hideTopBorder?: boolean } export function FeedItem({ post, record, reason, feedContext, moderation, parentAuthor, showReplyTo, isThreadChild, isThreadLastChild, isThreadParent, hideTopBorder, }: FeedItemProps & {post: 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 ( ) } return null } let FeedItemInner = ({ post, record, reason, feedContext, richText, moderation, parentAuthor, showReplyTo, isThreadChild, isThreadLastChild, isThreadParent, hideTopBorder, }: FeedItemProps & { richText: RichTextAPI post: Shadow }): 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 : hairlineWidth, }, ] const isParentBlocked = Boolean( parentAuthor?.viewer?.blockedBy || parentAuthor?.viewer?.blocking || parentAuthor?.viewer?.blockingByList, ) return ( {isThreadChild && ( )} {isReasonFeedSource(reason) ? ( From{' '} ) : AppBskyFeedDefs.isReasonRepost(reason) ? ( Reposted by{' '} ) : null} {isThreadParent && ( )} {!isThreadChild && showReplyTo && parentAuthor && ( )} ) } FeedItemInner = memo(FeedItemInner) let PostContent = ({ moderation, richText, postEmbed, postAuthor, onOpenEmbed, }: { moderation: ModerationDecision richText: RichTextAPI postEmbed: AppBskyFeedDefs.PostView['embed'] postAuthor: AppBskyFeedDefs.PostView['author'] onOpenEmbed: () => void }): React.ReactNode => { const pal = usePalette('default') const {_} = useLingui() const [limitLines, setLimitLines] = useState( () => countLines(richText.text) >= MAX_POST_LINES, ) const onPressShowMore = React.useCallback(() => { setLimitLines(false) }, [setLimitLines]) return ( {richText.text ? ( ) : undefined} {limitLines ? ( ) : undefined} {postEmbed ? ( ) : null} ) } PostContent = memo(PostContent) function ReplyToLabel({profile}: {profile: AppBskyActorDefs.ProfileViewBasic}) { const pal = usePalette('default') return ( Reply to{' '} ) } const styles = StyleSheet.create({ outer: { paddingLeft: 10, paddingRight: 15, // @ts-ignore web only -prf cursor: 'pointer', overflow: 'hidden', }, replyLine: { width: 2, marginLeft: 'auto', marginRight: 'auto', }, includeReason: { flexDirection: 'row', alignItems: 'center', marginTop: 2, marginBottom: 2, marginLeft: -18, }, layout: { flexDirection: 'row', marginTop: 1, gap: 10, }, layoutAvi: { paddingLeft: 8, 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, }, contentHiderChild: { marginTop: 6, }, embed: { marginBottom: 6, }, translateLink: { marginBottom: 6, }, })