import React, {useCallback, 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 {ModerationBehaviorCode} from 'lib/labeling/types' 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 {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' import {ImageHider} from '../util/moderation/ImageHider' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' import {sanitizeDisplayName} from 'lib/strings/display-names' export const FeedItem = observer(function ({ item, isThreadChild, isThreadParent, showFollowBtn, ignoreMuteFor, }: { item: PostsFeedItemModel isThreadChild?: boolean isThreadParent?: boolean showReplyLine?: boolean showFollowBtn?: boolean ignoreMuteFor?: string }) { 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 `/profile/${item.post.author.handle}/post/${urip.rkey}` }, [item.post.uri, item.post.author.handle]) const itemTitle = `Post by ${item.post.author.handle}` const authorHref = `/profile/${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 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 primaryLanguage = store.preferences.contentLanguages[0] || 'en' const onOpenTranslate = React.useCallback(() => { Linking.openURL( encodeURI( `https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${ record?.text || '' }`, ), ) }, [record, primaryLanguage]) 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 isSmallTop = isThreadChild const outerStyles = [ styles.outer, pal.view, {borderColor: pal.colors.border}, isSmallTop ? styles.outerSmallTop : undefined, isThreadParent ? styles.outerNoBottom : undefined, ] // moderation override let moderation = item.moderation.list if ( ignoreMuteFor === item.post.author.did && moderation.isMute && !moderation.noOverride ) { moderation = {behavior: ModerationBehaviorCode.Show} } const accessibilityActions = useMemo( () => [ { name: 'reply', label: 'Reply', }, { name: 'repost', label: item.post.viewer?.repost ? 'Undo repost' : 'Repost', }, {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'}, ], [item.post.viewer?.like, item.post.viewer?.repost], ) const onAccessibilityAction = useCallback( event => { switch (event.nativeEvent.actionName) { case 'like': onPressToggleLike() break case 'reply': onPressReply() break case 'repost': onPressToggleRepost() break default: break } }, [onPressReply, onPressToggleLike, onPressToggleRepost], ) if (!record || deleted) { return } return ( {isThreadChild && ( )} {isThreadParent && ( )} {item.reasonRepost && ( Reposted by{' '} )} {!isThreadChild && replyAuthorDid !== '' && ( Reply to{' '} )} {item.richText?.text ? ( ) : undefined} ) }) const styles = StyleSheet.create({ outer: { borderTopWidth: 1, padding: 10, paddingRight: 15, paddingBottom: 8, }, outerSmallTop: { borderTopWidth: 0, }, outerNoBottom: { paddingBottom: 2, }, topReplyLine: { position: 'absolute', left: 42, top: 0, height: 6, borderLeftWidth: 2, }, bottomReplyLine: { position: 'absolute', left: 42, top: 72, bottom: 0, borderLeftWidth: 2, }, includeReason: { flexDirection: 'row', paddingLeft: 50, paddingRight: 20, marginTop: 2, marginBottom: 2, }, includeReasonIcon: { marginRight: 4, }, layout: { flexDirection: 'row', marginTop: 1, }, layoutAvi: { width: 70, paddingLeft: 8, }, layoutContent: { flex: 1, }, postTextContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', paddingBottom: 4, }, contentHider: { marginTop: 4, }, embed: { marginBottom: 6, }, ctrls: { marginTop: 4, }, })