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 { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {PostThreadItemModel} from 'state/models/content/post-thread-item' import {Link} 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' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {pluralize} from 'lib/strings/helpers' 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 {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' import {PostSandboxWarning} from '../util/PostSandboxWarning' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' import {formatCount} from '../util/numeric/format' import {TimeElapsed} from 'view/com/util/TimeElapsed' import {makeProfileLink} from 'lib/routes/links' export const PostThreadItem = observer(function PostThreadItem({ item, onPostReply, }: { item: PostThreadItemModel onPostReply: () => void }) { const pal = usePalette('default') const store = useStores() const [deleted, setDeleted] = React.useState(false) const record = item.postRecord const hasEngagement = item.post.likeCount || item.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 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 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 repostsTitle = 'Reposts of this post' 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(() => { store.shell.openComposer({ replyTo: { uri: item.post.uri, cid: item.post.cid, text: record?.text as string, author: { handle: item.post.author.handle, displayName: item.post.author.displayName, avatar: item.post.author.avatar, }, }, onPost: onPostReply, }) }, [store, item, record, onPostReply]) const onPressToggleRepost = React.useCallback(() => { return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) }, [item, store]) const onPressToggleLike = React.useCallback(() => { return item .toggleLike() .catch(e => store.log.error('Failed to toggle like', e)) }, [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 () => { 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) } }, [item, store]) const onDeletePost = React.useCallback(() => { 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') }, ) }, [item, store]) if (!record) { return } if (deleted) { return ( This post has been deleted. ) } if (item._isHighlightedPost) { return ( <> {item.rootUri !== item.uri && ( )} {sanitizeDisplayName( item.post.author.displayName || sanitizeHandle(item.post.author.handle), )} ·  {({timeElapsed}) => <>{timeElapsed}} {sanitizeHandle(item.post.author.handle, '@')} {item.richText?.text ? ( ) : undefined} {item.post.embed && ( )} {hasEngagement ? ( {item.post.repostCount ? ( {formatCount(item.post.repostCount)} {' '} {pluralize(item.post.repostCount, 'repost')} ) : ( <> )} {item.post.likeCount ? ( {formatCount(item.post.likeCount)} {' '} {pluralize(item.post.likeCount, 'like')} ) : ( <> )} ) : ( <> )} ) } else { return ( <> {item._showParentReplyLine && ( )} {item._showChildReplyLine && ( )} {item.richText?.text ? ( ) : undefined} {item.post.embed && ( )} {needsTranslation && ( Translate this post )} {item._hasMore ? ( Continue thread... ) : undefined} ) } }) function ExpandedPostDetails({ post, needsTranslation, translatorUrl, }: { post: AppBskyFeedDefs.PostView needsTranslation: boolean translatorUrl: string }) { const pal = usePalette('default') return ( {niceDate(post.indexedAt)} {needsTranslation && ( <> Translate )} ) } const styles = StyleSheet.create({ outer: { borderTopWidth: 1, paddingLeft: 10, }, outerHighlighted: { paddingTop: 16, paddingLeft: 10, paddingRight: 10, }, noTopBorder: { borderTopWidth: 0, }, layout: { flexDirection: 'row', gap: 10, paddingLeft: 8, }, layoutAvi: {}, layoutContent: { flex: 1, paddingRight: 10, }, meta: { flexDirection: 'row', paddingTop: 2, paddingBottom: 2, }, metaExpandedLine1: { paddingTop: 5, paddingBottom: 0, }, metaItem: { paddingRight: 5, maxWidth: 240, }, alert: { marginBottom: 6, }, postTextContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', paddingBottom: 8, paddingRight: 10, }, postTextLargeContainer: { paddingHorizontal: 0, paddingBottom: 10, }, translateLink: { marginBottom: 6, }, contentHider: { marginBottom: 6, }, contentHiderChild: { marginTop: 6, }, expandedInfo: { flexDirection: 'row', padding: 10, borderTopWidth: 1, borderBottomWidth: 1, marginTop: 5, marginBottom: 15, }, expandedInfoItem: { marginRight: 10, }, loadMore: { flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, paddingLeft: 80, paddingRight: 20, paddingVertical: 12, }, replyLine: { width: 2, marginLeft: 'auto', marginRight: 'auto', }, })