import React, {memo} from 'react' import { Pressable, type PressableProps, type StyleProp, type ViewStyle, } from 'react-native' import * as Clipboard from 'expo-clipboard' import { AppBskyActorDefs, AppBskyFeedPost, AtUri, RichText as RichTextAPI, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {makeProfileLink} from '#/lib/routes/links' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' import {usePostDeleteMutation} from '#/state/queries/post' import {useSession} from '#/state/session' import {getCurrentRoute} from 'lib/routes/helpers' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {useTheme} from 'lib/ThemeContext' import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {EmbedDialog} from '#/components/dialogs/Embed' import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' import { EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, } from '#/components/icons/Emoji' import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import { isAvailable as isNativeTranslationAvailable, isLanguageSupported, NativeTranslationModule, } from '../../../../../modules/expo-bluesky-translate' import {EventStopper} from '../EventStopper' import * as Toast from '../Toast' let PostDropdownBtn = ({ testID, postAuthor, postCid, postUri, postFeedContext, record, richText, style, hitSlop, size, timestamp, }: { testID: string postAuthor: AppBskyActorDefs.ProfileViewBasic postCid: string postUri: string postFeedContext: string | undefined record: AppBskyFeedPost.Record richText: RichTextAPI style?: StyleProp hitSlop?: PressableProps['hitSlop'] size?: 'lg' | 'md' | 'sm' timestamp: string }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() const alf = useAlf() const {gtMobile} = useBreakpoints() const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const langPrefs = useLanguagePrefs() const mutedThreads = useMutedThreads() const toggleThreadMute = useToggleThreadMute() const postDeleteMutation = usePostDeleteMutation() const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() const feedFeedback = useFeedFeedbackContext() const openLink = useOpenLink() const navigation = useNavigation() const {mutedWordsDialogControl} = useGlobalDialogsControlContext() const reportDialogControl = useReportDialogControl() const deletePromptControl = useDialogControl() const hidePromptControl = useDialogControl() const loggedOutWarningPromptControl = useDialogControl() const embedPostControl = useDialogControl() const sendViaChatControl = useDialogControl() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) const isAuthor = postAuthor.did === currentAccount?.did const href = React.useMemo(() => { const urip = new AtUri(postUri) return makeProfileLink(postAuthor, 'post', urip.rkey) }, [postUri, postAuthor]) const translatorUrl = getTranslatorLink( record.text, langPrefs.primaryLanguage, ) const onDeletePost = React.useCallback(() => { postDeleteMutation.mutateAsync({uri: postUri}).then( () => { Toast.show(_(msg`Post deleted`)) const route = getCurrentRoute(navigation.getState()) if (route.name === 'PostThread') { const params = route.params as CommonNavigatorParams['PostThread'] if ( currentAccount && isAuthor && (params.name === currentAccount.handle || params.name === currentAccount.did) ) { const currentHref = makeProfileLink(postAuthor, 'post', params.rkey) if (currentHref === href && navigation.canGoBack()) { navigation.goBack() } } } }, e => { logger.error('Failed to delete post', {message: e}) Toast.show(_(msg`Failed to delete post, please try again`)) }, ) }, [ navigation, postUri, postDeleteMutation, postAuthor, currentAccount, isAuthor, href, _, ]) const onToggleThreadMute = React.useCallback(() => { try { const muted = toggleThreadMute(rootUri) if (muted) { Toast.show( _(msg`You will no longer receive notifications for this thread`), ) } else { Toast.show(_(msg`You will now receive notifications for this thread`)) } } catch (e) { logger.error('Failed to toggle thread mute', {message: e}) } }, [rootUri, toggleThreadMute, _]) const onCopyPostText = React.useCallback(() => { const str = richTextToString(richText, true) Clipboard.setStringAsync(str) Toast.show(_(msg`Copied to clipboard`)) }, [_, richText]) const onPressTranslate = React.useCallback(() => { if ( isNativeTranslationAvailable && isLanguageSupported(record?.langs?.at(0)) ) { const text = richTextToString(richText, true) NativeTranslationModule.presentAsync(text) } else { openLink(translatorUrl) } }, [openLink, record?.langs, richText, translatorUrl]) const onHidePost = React.useCallback(() => { hidePost({uri: postUri}) }, [postUri, hidePost]) const hideInPWI = React.useMemo(() => { return !!postAuthor.labels?.find( label => label.val === '!no-unauthenticated', ) }, [postAuthor]) const onSharePost = React.useCallback(() => { const url = toShareUrl(href) shareUrl(url) }, [href]) const onPressShowMore = React.useCallback(() => { feedFeedback.sendInteraction({ event: 'app.bsky.feed.defs#requestMore', item: postUri, feedContext: postFeedContext, }) Toast.show('Feedback sent!') }, [feedFeedback, postUri, postFeedContext]) const onPressShowLess = React.useCallback(() => { feedFeedback.sendInteraction({ event: 'app.bsky.feed.defs#requestLess', item: postUri, feedContext: postFeedContext, }) Toast.show('Feedback sent!') }, [feedFeedback, postUri, postFeedContext]) const onSelectChatToShareTo = React.useCallback( (conversation: string) => { navigation.navigate('MessagesConversation', { conversation, embed: postUri, }) }, [navigation, postUri], ) const canEmbed = isWeb && gtMobile && !hideInPWI return ( {({props, state}) => { return ( ) }} {(!hideInPWI || hasSession) && ( <> {_(msg`Translate`)} {_(msg`Copy post text`)} )} {hasSession && ( Send via direct message )} { if (hideInPWI) { loggedOutWarningPromptControl.open() } else { onSharePost() } }}> {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} {canEmbed && ( {_(msg`Embed post`)} )} {hasSession && feedFeedback.enabled && ( <> {_(msg`Show more like this`)} {_(msg`Show less like this`)} )} {hasSession && ( <> {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)} mutedWordsDialogControl.open()}> {_(msg`Mute words & tags`)} {!isAuthor && !isPostHidden && ( {_(msg`Hide post`)} )} )} {hasSession && ( <> {!isAuthor && ( reportDialogControl.open()}> {_(msg`Report post`)} )} {isAuthor && ( {_(msg`Delete post`)} )} )} ) } PostDropdownBtn = memo(PostDropdownBtn) export {PostDropdownBtn}