import React, {useMemo, useState} from 'react' import {observer} from 'mobx-react-lite' import {Linking, StyleSheet, View} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {AtUri} from '@atproto/api' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {PostsFeedItemModel} from 'state/models/feeds/post' import {Link, DesktopWebTextLink} from '../util/Link' import {Text} from '../util/text/Text' import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostEmbeds} from '../util/post-embeds' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' import {RichText} from '../util/text/RichText' import {PostSandboxWarning} from '../util/PostSandboxWarning' import * as Toast from '../util/Toast' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {makeProfileLink} from 'lib/routes/links' import {isEmbedByEmbedder} from 'lib/embeds' export const FeedItem = observer(function ({ item, isThreadChild, isThreadLastChild, isThreadParent, }: { item: PostsFeedItemModel isThreadChild?: boolean isThreadLastChild?: boolean isThreadParent?: boolean showReplyLine?: boolean }) { const store = useStores() const pal = usePalette('default') const {track} = useAnalytics() const [deleted, setDeleted] = useState(false) const record = item.postRecord const itemUri = item.post.uri const itemCid = item.post.cid const itemHref = 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 replyAuthorDid = useMemo(() => { if (!record?.reply) { return '' } const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) return urip.hostname }, [record?.reply]) const translatorUrl = getTranslatorLink(record?.text || '') const needsTranslation = useMemo( () => store.preferences.contentLanguages.length > 0 && !isPostInLanguage(item.post, store.preferences.contentLanguages), [item.post, store.preferences.contentLanguages], ) const onPressReply = React.useCallback(() => { track('FeedItem:PostReply') store.shell.openComposer({ replyTo: { uri: item.post.uri, cid: item.post.cid, text: record?.text || '', author: { handle: item.post.author.handle, displayName: item.post.author.displayName, avatar: item.post.author.avatar, }, }, }) }, [item, track, record, store]) const onPressToggleRepost = React.useCallback(() => { track('FeedItem:PostRepost') return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) }, [track, item, store]) const onPressToggleLike = React.useCallback(() => { track('FeedItem:PostLike') return item .toggleLike() .catch(e => store.log.error('Failed to toggle like', e)) }, [track, item, store]) 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 () => { track('FeedItem:ThreadMute') 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) { store.log.error('Failed to toggle thread mute', e) } }, [track, item, store]) const onDeletePost = React.useCallback(() => { track('FeedItem:PostDelete') item.delete().then( () => { setDeleted(true) Toast.show('Post deleted') }, e => { store.log.error('Failed to delete post', e) Toast.show('Failed to delete post, please try again') }, ) }, [track, item, setDeleted, store]) const outerStyles = [ styles.outer, pal.view, { borderColor: pal.colors.border, paddingBottom: isThreadLastChild || (!isThreadChild && !isThreadParent) ? 6 : undefined, }, isThreadChild ? styles.outerSmallTop : undefined, ] if (!record || deleted) { return } return ( {isThreadChild && ( )} {item.reasonRepost && ( Reposted by{' '} )} {isThreadParent && ( )} {!isThreadChild && replyAuthorDid !== '' && ( Reply to{' '} )} {item.richText?.text ? ( ) : undefined} {item.post.embed ? ( ) : null} {needsTranslation && ( Translate this post )} ) }) const styles = StyleSheet.create({ outer: { borderTopWidth: 1, paddingLeft: 10, paddingRight: 15, cursor: 'pointer', }, outerSmallTop: { borderTopWidth: 0, }, replyLine: { width: 2, marginLeft: 'auto', marginRight: 'auto', }, includeReason: { flexDirection: 'row', marginTop: 2, marginBottom: 4, marginLeft: -20, }, includeReasonIcon: { marginRight: 4, }, layout: { flexDirection: 'row', marginTop: 1, gap: 10, }, layoutAvi: { paddingLeft: 8, }, layoutContent: { flex: 1, }, alert: { marginTop: 6, marginBottom: 6, }, postTextContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', paddingBottom: 4, }, contentHiderChild: { marginTop: 6, }, embed: { marginBottom: 6, }, translateLink: { marginBottom: 6, }, })