diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 6 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 22 | ||||
-rw-r--r-- | src/view/com/posts/PostFeedItem.tsx | 9 | ||||
-rw-r--r-- | src/view/com/profile/ProfileMenu.tsx | 29 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 118 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtnMenuItems.tsx | 872 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/PostCtrls.tsx | 394 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/RepostButton.tsx | 221 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/RepostButton.web.tsx | 147 |
9 files changed, 39 insertions, 1779 deletions
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 3925ce9bd..82852aa62 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -43,7 +43,6 @@ import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {Link, TextLink} from '#/view/com/util/Link' import {formatCount} from '#/view/com/util/numeric/format' -import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -60,6 +59,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' +import {PostControls} from '#/components/PostControls' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {SubtleWebHover} from '#/components/SubtleWebHover' @@ -494,7 +494,7 @@ let PostThreadItemLoaded = ({ marginLeft: -5, }, ]}> - <PostCtrls + <PostControls big post={post} record={record} @@ -642,7 +642,7 @@ let PostThreadItemLoaded = ({ /> </View> )} - <PostCtrls + <PostControls post={post} record={record} richText={richText} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 03463f977..1a48d64d8 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -27,21 +27,21 @@ import { import {useModerationOpts} from '#/state/preferences/moderation-opts' import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' +import {Link, TextLink} from '#/view/com/util/Link' +import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' +import {PostMeta} from '#/view/com/util/PostMeta' +import {Text} from '#/view/com/util/text/Text' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {UserInfoText} from '#/view/com/util/UserInfoText' import {atoms as a} from '#/alf' +import {ContentHider} from '#/components/moderation/ContentHider' +import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {PostControls} from '#/components/PostControls' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' import {SubtleWebHover} from '#/components/SubtleWebHover' import * as bsky from '#/types/bsky' -import {ContentHider} from '../../../components/moderation/ContentHider' -import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' -import {PostAlerts} from '../../../components/moderation/PostAlerts' -import {Link, TextLink} from '../util/Link' -import {PostCtrls} from '../util/post-ctrls/PostCtrls' -import {PostEmbeds, PostEmbedViewContext} from '../util/post-embeds' -import {PostMeta} from '../util/PostMeta' -import {Text} from '../util/text/Text' -import {PreviewableUserAvatar} from '../util/UserAvatar' -import {UserInfoText} from '../util/UserInfoText' export function Post({ post, @@ -255,7 +255,7 @@ function PostInner({ /> ) : null} </ContentHider> - <PostCtrls + <PostControls post={post} record={record} richText={richText} diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 2cc749404..3735bbb5a 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -37,7 +37,7 @@ import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {FeedNameText} from '#/view/com/util/FeedInfoText' -import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' +import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {Text} from '#/view/com/util/text/Text' @@ -49,11 +49,12 @@ import {ContentHider} from '#/components/moderation/ContentHider' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' +import {PostControls} from '#/components/PostControls' +import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' import {SubtleWebHover} from '#/components/SubtleWebHover' import * as bsky from '#/types/bsky' -import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' interface FeedItemProps { record: AppBskyFeedPost.Record @@ -439,7 +440,7 @@ let FeedItemInner = ({ post={post} threadgateRecord={threadgateRecord} /> - <PostCtrls + <PostControls post={post} record={record} richText={richText} @@ -451,6 +452,8 @@ let FeedItemInner = ({ onShowLess={onShowLess} /> </View> + + <DiscoverDebug feedContext={feedContext} /> </View> </Link> ) diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index f1fd237ec..d18ba12c1 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -12,6 +12,7 @@ import {type NavigationProp} from '#/lib/routes/types' import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl} from '#/lib/strings/url-helpers' import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' import {type Shadow} from '#/state/cache/types' import {useModalControls} from '#/state/modals' import { @@ -26,9 +27,11 @@ import {EventStopper} from '#/view/com/util/EventStopper' import * as Toast from '#/view/com/util/Toast' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' -import {CircleX_Stroke2_Corner0_Rounded as CircleX} from '#/components/icons/CircleX' +import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' +import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' +import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' +import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' +import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' @@ -236,7 +239,9 @@ let ProfileMenu = ({ <Menu.Group> <Menu.Item testID="profileHeaderDropdownShareBtn" - label={_(msg`Share`)} + label={ + isWeb ? _(msg`Copy link to profile`) : _(msg`Share via...`) + } onPress={() => { if (showLoggedOutWarning) { loggedOutWarningPromptControl.open() @@ -245,9 +250,13 @@ let ProfileMenu = ({ } }}> <Menu.ItemText> - <Trans>Share</Trans> + {isWeb ? ( + <Trans>Copy link to profile</Trans> + ) : ( + <Trans>Share via...</Trans> + )} </Menu.ItemText> - <Menu.ItemIcon icon={Share} /> + <Menu.ItemIcon icon={isWeb ? ChainLinkIcon : ArrowOutOfBoxIcon} /> </Menu.Item> <Menu.Item testID="profileHeaderDropdownSearchBtn" @@ -329,7 +338,7 @@ let ProfileMenu = ({ <Menu.ItemText> <Trans>Remove verification</Trans> </Menu.ItemText> - <Menu.ItemIcon icon={CircleX} /> + <Menu.ItemIcon icon={CircleXIcon} /> </Menu.Item> ) : ( <Menu.Item @@ -339,7 +348,7 @@ let ProfileMenu = ({ <Menu.ItemText> <Trans>Verify account</Trans> </Menu.ItemText> - <Menu.ItemIcon icon={CircleCheck} /> + <Menu.ItemIcon icon={CircleCheckIcon} /> </Menu.Item> ))} {!isSelf && ( @@ -414,7 +423,7 @@ let ProfileMenu = ({ <Menu.ItemText> <Trans>Copy at:// URI</Trans> </Menu.ItemText> - <Menu.ItemIcon icon={Share} /> + <Menu.ItemIcon icon={ClipboardIcon} /> </Menu.Item> <Menu.Item testID="profileHeaderDropdownShareDIDBtn" @@ -423,7 +432,7 @@ let ProfileMenu = ({ <Menu.ItemText> <Trans>Copy DID</Trans> </Menu.ItemText> - <Menu.ItemIcon icon={Share} /> + <Menu.ItemIcon icon={ClipboardIcon} /> </Menu.Item> </Menu.Group> </> diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx deleted file mode 100644 index 57ee95e31..000000000 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import {memo, useMemo, useState} from 'react' -import { - Pressable, - type PressableProps, - type StyleProp, - type ViewStyle, -} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyFeedPost, - type AppBskyFeedThreadgate, - type RichText as RichTextAPI, -} from '@atproto/api' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import type React from 'react' - -import {useTheme} from '#/lib/ThemeContext' -import {type Shadow} from '#/state/cache/post-shadow' -import {atoms as a, useTheme as useAlf} from '#/alf' -import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' -import {useMenuControl} from '#/components/Menu' -import * as Menu from '#/components/Menu' -import {EventStopper} from '../EventStopper' -import {PostDropdownMenuItems} from './PostDropdownBtnMenuItems' - -let PostDropdownBtn = ({ - testID, - post, - postFeedContext, - postReqId, - record, - richText, - style, - hitSlop, - size, - timestamp, - threadgateRecord, - onShowLess, -}: { - testID: string - post: Shadow<AppBskyFeedDefs.PostView> - postFeedContext: string | undefined - postReqId: string | undefined - record: AppBskyFeedPost.Record - richText: RichTextAPI - style?: StyleProp<ViewStyle> - hitSlop?: PressableProps['hitSlop'] - size?: 'lg' | 'md' | 'sm' - timestamp: string - threadgateRecord?: AppBskyFeedThreadgate.Record - onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void -}): React.ReactNode => { - const theme = useTheme() - const alf = useAlf() - const {_} = useLingui() - const defaultCtrlColor = theme.palette.default.postCtrl - const menuControl = useMenuControl() - const [hasBeenOpen, setHasBeenOpen] = useState(false) - const lazyMenuControl = useMemo( - () => ({ - ...menuControl, - open() { - setHasBeenOpen(true) - // HACK. We need the state update to be flushed by the time - // menuControl.open() fires but RN doesn't expose flushSync. - setTimeout(menuControl.open) - }, - }), - [menuControl, setHasBeenOpen], - ) - return ( - <EventStopper onKeyDown={false}> - <Menu.Root control={lazyMenuControl}> - <Menu.Trigger label={_(msg`Open post options menu`)}> - {({props, state}) => { - return ( - <Pressable - {...props} - hitSlop={hitSlop} - testID={testID} - style={[ - style, - a.rounded_full, - (state.hovered || state.pressed) && [ - alf.atoms.bg_contrast_25, - ], - ]}> - <DotsHorizontal - fill={defaultCtrlColor} - style={{pointerEvents: 'none'}} - size={size} - /> - </Pressable> - ) - }} - </Menu.Trigger> - {hasBeenOpen && ( - // Lazily initialized. Once mounted, they stay mounted. - <PostDropdownMenuItems - testID={testID} - post={post} - postFeedContext={postFeedContext} - postReqId={postReqId} - record={record} - richText={richText} - timestamp={timestamp} - threadgateRecord={threadgateRecord} - onShowLess={onShowLess} - /> - )} - </Menu.Root> - </EventStopper> - ) -} - -PostDropdownBtn = memo(PostDropdownBtn) -export {PostDropdownBtn} diff --git a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx deleted file mode 100644 index a5f41ea7a..000000000 --- a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx +++ /dev/null @@ -1,872 +0,0 @@ -import React, {memo} from 'react' -import { - Platform, - type PressableProps, - type StyleProp, - type ViewStyle, -} from 'react-native' -import * as Clipboard from 'expo-clipboard' -import { - type AppBskyFeedDefs, - AppBskyFeedPost, - type AppBskyFeedThreadgate, - AtUri, - type RichText as RichTextAPI, -} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useNavigation} from '@react-navigation/native' - -import {IS_INTERNAL} from '#/lib/app-info' -import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' -import {useOpenLink} from '#/lib/hooks/useOpenLink' -import {getCurrentRoute} from '#/lib/routes/helpers' -import {makeProfileLink} from '#/lib/routes/links' -import { - type CommonNavigatorParams, - type NavigationProp, -} from '#/lib/routes/types' -import {shareText, shareUrl} from '#/lib/sharing' -import {logEvent} from '#/lib/statsig/statsig' -import {richTextToString} from '#/lib/strings/rich-text-helpers' -import {toShareUrl} from '#/lib/strings/url-helpers' -import {getTranslatorLink} from '#/locale/helpers' -import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' -import {type Shadow} from '#/state/cache/post-shadow' -import {useProfileShadow} from '#/state/cache/profile-shadow' -import {useFeedFeedbackContext} from '#/state/feed-feedback' -import {useLanguagePrefs} from '#/state/preferences' -import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' -import {usePinnedPostMutation} from '#/state/queries/pinned-post' -import { - usePostDeleteMutation, - useThreadMuteMutationQueue, -} from '#/state/queries/post' -import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate' -import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util' -import { - useProfileBlockMutationQueue, - useProfileMuteMutationQueue, -} from '#/state/queries/profile' -import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate' -import {useSession} from '#/state/session' -import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {useBreakpoints} from '#/alf' -import {useDialogControl} from '#/components/Dialog' -import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' -import {EmbedDialog} from '#/components/dialogs/Embed' -import { - PostInteractionSettingsDialog, - usePrefetchPostInteractionSettings, -} from '#/components/dialogs/PostInteractionSettingsDialog' -import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' -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 { - EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, - EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, -} from '#/components/icons/Emoji' -import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye' -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 MuteIcon} from '#/components/icons/Mute' -import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' -import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane' -import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person' -import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' -import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' -import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' -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 {Loader} from '#/components/Loader' -import * as Menu from '#/components/Menu' -import { - ReportDialog, - useReportDialogControl, -} from '#/components/moderation/ReportDialog' -import * as Prompt from '#/components/Prompt' -import {useDevMode} from '#/storage/hooks/dev-mode' -import * as bsky from '#/types/bsky' -import * as Toast from '../Toast' - -let PostDropdownMenuItems = ({ - post, - postFeedContext, - postReqId, - record, - richText, - timestamp, - threadgateRecord, - onShowLess, -}: { - testID: string - post: Shadow<AppBskyFeedDefs.PostView> - postFeedContext: string | undefined - postReqId: string | undefined - record: AppBskyFeedPost.Record - richText: RichTextAPI - style?: StyleProp<ViewStyle> - hitSlop?: PressableProps['hitSlop'] - size?: 'lg' | 'md' | 'sm' - timestamp: string - threadgateRecord?: AppBskyFeedThreadgate.Record - onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void -}): React.ReactNode => { - const {hasSession, currentAccount} = useSession() - const {gtMobile} = useBreakpoints() - const {_} = useLingui() - const langPrefs = useLanguagePrefs() - const {mutateAsync: deletePostMutate} = usePostDeleteMutation() - const {mutateAsync: pinPostMutate, isPending: isPinPending} = - usePinnedPostMutation() - const hiddenPosts = useHiddenPosts() - const {hidePost} = useHiddenPostsApi() - const feedFeedback = useFeedFeedbackContext() - const openLink = useOpenLink() - const navigation = useNavigation<NavigationProp>() - const {mutedWordsDialogControl} = useGlobalDialogsControlContext() - const blockPromptControl = useDialogControl() - const reportDialogControl = useReportDialogControl() - const deletePromptControl = useDialogControl() - const hidePromptControl = useDialogControl() - const loggedOutWarningPromptControl = useDialogControl() - const embedPostControl = useDialogControl() - const sendViaChatControl = useDialogControl() - const postInteractionSettingsDialogControl = useDialogControl() - const quotePostDetachConfirmControl = useDialogControl() - const hideReplyConfirmControl = useDialogControl() - const {mutateAsync: toggleReplyVisibility} = - useToggleReplyVisibilityMutation() - const [devModeEnabled] = useDevMode() - - const postUri = post.uri - const postCid = post.cid - const postAuthor = useProfileShadow(post.author) - const quoteEmbed = React.useMemo(() => { - if (!currentAccount || !post.embed) return - return getMaybeDetachedQuoteEmbed({ - viewerDid: currentAccount.did, - post, - }) - }, [post, currentAccount]) - - const rootUri = record.reply?.root?.uri || postUri - const isReply = Boolean(record.reply) - const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue( - post, - rootUri, - ) - const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) - const isAuthor = postAuthor.did === currentAccount?.did - const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did - const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ - threadgateRecord, - }) - const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri) - const isPinned = post.viewer?.pinned - - const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} = - useToggleQuoteDetachmentMutation() - - const [queueBlock] = useProfileBlockMutationQueue(postAuthor) - const [queueMute, queueUnmute] = useProfileMuteMutationQueue(postAuthor) - - const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ - postUri: post.uri, - rootPostUri: rootUri, - }) - - 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 = () => { - deletePostMutate({uri: postUri}).then( - () => { - Toast.show(_(msg({message: 'Post deleted', context: 'toast'}))) - - 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`), 'xmark') - }, - ) - } - - const onToggleThreadMute = () => { - try { - if (isThreadMuted) { - unmuteThread() - Toast.show(_(msg`You will now receive notifications for this thread`)) - } else { - muteThread() - Toast.show( - _(msg`You will no longer receive notifications for this thread`), - ) - } - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to toggle thread mute', {message: e}) - Toast.show( - _(msg`Failed to toggle thread mute, please try again`), - 'xmark', - ) - } - } - } - - const onCopyPostText = () => { - const str = richTextToString(richText, true) - - Clipboard.setStringAsync(str) - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') - } - - const onPressTranslate = async () => { - await openLink(translatorUrl, true) - - if ( - bsky.dangerousIsType<AppBskyFeedPost.Record>( - post.record, - AppBskyFeedPost.isRecord, - ) - ) { - logger.metric('translate', { - sourceLanguages: post.record.langs ?? [], - targetLanguage: langPrefs.primaryLanguage, - textLength: post.record.text.length, - }) - } - } - - const onHidePost = () => { - hidePost({uri: postUri}) - } - - const hideInPWI = !!postAuthor.labels?.find( - label => label.val === '!no-unauthenticated', - ) - - const showLoggedOutWarning = - postAuthor.did !== currentAccount?.did && hideInPWI - - const onSharePost = () => { - const url = toShareUrl(href) - shareUrl(url) - } - - const onPressShowMore = () => { - feedFeedback.sendInteraction({ - event: 'app.bsky.feed.defs#requestMore', - item: postUri, - feedContext: postFeedContext, - reqId: postReqId, - }) - Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) - } - - const onPressShowLess = () => { - feedFeedback.sendInteraction({ - event: 'app.bsky.feed.defs#requestLess', - item: postUri, - feedContext: postFeedContext, - reqId: postReqId, - }) - if (onShowLess) { - onShowLess({ - item: postUri, - feedContext: postFeedContext, - }) - } else { - Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) - } - } - - const onSelectChatToShareTo = (conversation: string) => { - navigation.navigate('MessagesConversation', { - conversation, - embed: postUri, - }) - } - - const onToggleQuotePostAttachment = async () => { - if (!quoteEmbed) return - - const action = quoteEmbed.isDetached ? 'reattach' : 'detach' - const isDetach = action === 'detach' - - try { - await toggleQuoteDetachment({ - post, - quoteUri: quoteEmbed.uri, - action: quoteEmbed.isDetached ? 'reattach' : 'detach', - }) - Toast.show( - isDetach - ? _(msg`Quote post was successfully detached`) - : _(msg`Quote post was re-attached`), - ) - } catch (e: any) { - Toast.show( - _(msg({message: 'Updating quote attachment failed', context: 'toast'})), - ) - logger.error(`Failed to ${action} quote`, {safeMessage: e.message}) - } - } - - const canHidePostForMe = !isAuthor && !isPostHidden - const canEmbed = isWeb && gtMobile && !hideInPWI - const canHideReplyForEveryone = - !isAuthor && isRootPostAuthor && !isPostHidden && isReply - const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer - - const onToggleReplyVisibility = async () => { - // TODO no threadgate? - if (!canHideReplyForEveryone) return - - const action = isReplyHiddenByThreadgate ? 'show' : 'hide' - const isHide = action === 'hide' - - try { - await toggleReplyVisibility({ - postUri: rootUri, - replyUri: postUri, - action, - }) - Toast.show( - isHide - ? _(msg`Reply was successfully hidden`) - : _(msg({message: 'Reply visibility updated', context: 'toast'})), - ) - } catch (e: any) { - Toast.show( - _(msg({message: 'Updating reply visibility failed', context: 'toast'})), - ) - logger.error(`Failed to ${action} reply`, {safeMessage: e.message}) - } - } - - const onPressPin = () => { - logEvent(isPinned ? 'post:unpin' : 'post:pin', {}) - pinPostMutate({ - postUri, - postCid, - action: isPinned ? 'unpin' : 'pin', - }) - } - - const onBlockAuthor = async () => { - try { - await queueBlock() - Toast.show(_(msg({message: 'Account blocked', context: 'toast'}))) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to block account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') - } - } - } - - const onMuteAuthor = async () => { - if (postAuthor.viewer?.muted) { - try { - await queueUnmute() - Toast.show(_(msg({message: 'Account unmuted', context: 'toast'}))) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to unmute account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') - } - } - } else { - try { - await queueMute() - Toast.show(_(msg({message: 'Account muted', context: 'toast'}))) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to mute account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') - } - } - } - } - - const onShareATURI = () => { - shareText(postUri) - } - - const onShareAuthorDID = () => { - shareText(postAuthor.did) - } - - const onReportMisclassification = () => { - const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl( - href, - )}` - openLink(url) - } - - return ( - <> - <Menu.Outer> - {isAuthor && ( - <> - <Menu.Group> - <Menu.Item - testID="pinPostBtn" - label={ - isPinned - ? _(msg`Unpin from profile`) - : _(msg`Pin to your profile`) - } - disabled={isPinPending} - onPress={onPressPin}> - <Menu.ItemText> - {isPinned - ? _(msg`Unpin from profile`) - : _(msg`Pin to your profile`)} - </Menu.ItemText> - <Menu.ItemIcon - icon={isPinPending ? Loader : PinIcon} - position="right" - /> - </Menu.Item> - </Menu.Group> - <Menu.Divider /> - </> - )} - - <Menu.Group> - {(!hideInPWI || hasSession) && ( - <> - <Menu.Item - testID="postDropdownTranslateBtn" - label={_(msg`Translate`)} - onPress={onPressTranslate}> - <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText> - <Menu.ItemIcon icon={Translate} position="right" /> - </Menu.Item> - - <Menu.Item - testID="postDropdownCopyTextBtn" - label={_(msg`Copy post text`)} - onPress={onCopyPostText}> - <Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText> - <Menu.ItemIcon icon={ClipboardIcon} position="right" /> - </Menu.Item> - </> - )} - - {hasSession && ( - <Menu.Item - testID="postDropdownSendViaDMBtn" - label={_(msg`Send via direct message`)} - onPress={() => sendViaChatControl.open()}> - <Menu.ItemText> - <Trans>Send via direct message</Trans> - </Menu.ItemText> - <Menu.ItemIcon icon={Send} position="right" /> - </Menu.Item> - )} - - <Menu.Item - testID="postDropdownShareBtn" - label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} - onPress={() => { - if (showLoggedOutWarning) { - loggedOutWarningPromptControl.open() - } else { - onSharePost() - } - }}> - <Menu.ItemText> - {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} - </Menu.ItemText> - <Menu.ItemIcon icon={Share} position="right" /> - </Menu.Item> - - {canEmbed && ( - <Menu.Item - testID="postDropdownEmbedBtn" - label={_(msg`Embed post`)} - onPress={() => embedPostControl.open()}> - <Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText> - <Menu.ItemIcon icon={CodeBrackets} position="right" /> - </Menu.Item> - )} - </Menu.Group> - - {hasSession && feedFeedback.enabled && ( - <> - <Menu.Divider /> - <Menu.Group> - <Menu.Item - testID="postDropdownShowMoreBtn" - label={_(msg`Show more like this`)} - onPress={onPressShowMore}> - <Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText> - <Menu.ItemIcon icon={EmojiSmile} position="right" /> - </Menu.Item> - - <Menu.Item - testID="postDropdownShowLessBtn" - label={_(msg`Show less like this`)} - onPress={onPressShowLess}> - <Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText> - <Menu.ItemIcon icon={EmojiSad} position="right" /> - </Menu.Item> - </Menu.Group> - </> - )} - - {hasSession && - IS_INTERNAL && - DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && ( - <Menu.Item - testID="postDropdownReportMisclassificationBtn" - label={_(msg`Assign topic - help train Discover!`)} - onPress={onReportMisclassification}> - <Menu.ItemText> - {_(msg`Assign topic - help train Discover!`)} - </Menu.ItemText> - <Menu.ItemIcon icon={AtomIcon} position="right" /> - </Menu.Item> - )} - - {hasSession && ( - <> - <Menu.Divider /> - <Menu.Group> - <Menu.Item - testID="postDropdownMuteThreadBtn" - label={ - isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`) - } - onPress={onToggleThreadMute}> - <Menu.ItemText> - {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)} - </Menu.ItemText> - <Menu.ItemIcon - icon={isThreadMuted ? Unmute : Mute} - position="right" - /> - </Menu.Item> - - <Menu.Item - testID="postDropdownMuteWordsBtn" - label={_(msg`Mute words & tags`)} - onPress={() => mutedWordsDialogControl.open()}> - <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText> - <Menu.ItemIcon icon={Filter} position="right" /> - </Menu.Item> - </Menu.Group> - </> - )} - - {hasSession && - (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && ( - <> - <Menu.Divider /> - <Menu.Group> - {canHidePostForMe && ( - <Menu.Item - testID="postDropdownHideBtn" - label={ - isReply - ? _(msg`Hide reply for me`) - : _(msg`Hide post for me`) - } - onPress={() => hidePromptControl.open()}> - <Menu.ItemText> - {isReply - ? _(msg`Hide reply for me`) - : _(msg`Hide post for me`)} - </Menu.ItemText> - <Menu.ItemIcon icon={EyeSlash} position="right" /> - </Menu.Item> - )} - {canHideReplyForEveryone && ( - <Menu.Item - testID="postDropdownHideBtn" - label={ - isReplyHiddenByThreadgate - ? _(msg`Show reply for everyone`) - : _(msg`Hide reply for everyone`) - } - onPress={ - isReplyHiddenByThreadgate - ? onToggleReplyVisibility - : () => hideReplyConfirmControl.open() - }> - <Menu.ItemText> - {isReplyHiddenByThreadgate - ? _(msg`Show reply for everyone`) - : _(msg`Hide reply for everyone`)} - </Menu.ItemText> - <Menu.ItemIcon - icon={isReplyHiddenByThreadgate ? Eye : EyeSlash} - position="right" - /> - </Menu.Item> - )} - - {canDetachQuote && ( - <Menu.Item - disabled={isDetachPending} - testID="postDropdownHideBtn" - label={ - quoteEmbed.isDetached - ? _(msg`Re-attach quote`) - : _(msg`Detach quote`) - } - onPress={ - quoteEmbed.isDetached - ? onToggleQuotePostAttachment - : () => quotePostDetachConfirmControl.open() - }> - <Menu.ItemText> - {quoteEmbed.isDetached - ? _(msg`Re-attach quote`) - : _(msg`Detach quote`)} - </Menu.ItemText> - <Menu.ItemIcon - icon={ - isDetachPending - ? Loader - : quoteEmbed.isDetached - ? Eye - : EyeSlash - } - position="right" - /> - </Menu.Item> - )} - </Menu.Group> - </> - )} - - {hasSession && ( - <> - <Menu.Divider /> - <Menu.Group> - {!isAuthor && ( - <> - <Menu.Item - testID="postDropdownMuteBtn" - label={ - postAuthor.viewer?.muted - ? _(msg`Unmute account`) - : _(msg`Mute account`) - } - onPress={onMuteAuthor}> - <Menu.ItemText> - {postAuthor.viewer?.muted - ? _(msg`Unmute account`) - : _(msg`Mute account`)} - </Menu.ItemText> - <Menu.ItemIcon - icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon} - position="right" - /> - </Menu.Item> - - {!postAuthor.viewer?.blocking && ( - <Menu.Item - testID="postDropdownBlockBtn" - label={_(msg`Block account`)} - onPress={() => blockPromptControl.open()}> - <Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText> - <Menu.ItemIcon icon={PersonX} position="right" /> - </Menu.Item> - )} - - <Menu.Item - testID="postDropdownReportBtn" - label={_(msg`Report post`)} - onPress={() => reportDialogControl.open()}> - <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText> - <Menu.ItemIcon icon={Warning} position="right" /> - </Menu.Item> - </> - )} - - {isAuthor && ( - <> - <Menu.Item - testID="postDropdownEditPostInteractions" - label={_(msg`Edit interaction settings`)} - onPress={() => postInteractionSettingsDialogControl.open()} - {...(isAuthor - ? Platform.select({ - web: { - onHoverIn: prefetchPostInteractionSettings, - }, - native: { - onPressIn: prefetchPostInteractionSettings, - }, - }) - : {})}> - <Menu.ItemText> - {_(msg`Edit interaction settings`)} - </Menu.ItemText> - <Menu.ItemIcon icon={Gear} position="right" /> - </Menu.Item> - <Menu.Item - testID="postDropdownDeleteBtn" - label={_(msg`Delete post`)} - onPress={() => deletePromptControl.open()}> - <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> - <Menu.ItemIcon icon={Trash} position="right" /> - </Menu.Item> - </> - )} - </Menu.Group> - - {devModeEnabled ? ( - <> - <Menu.Divider /> - <Menu.Group> - <Menu.Item - testID="postAtUriShareBtn" - label={_(msg`Copy post at:// URI`)} - onPress={onShareATURI}> - <Menu.ItemText>{_(msg`Copy post at:// URI`)}</Menu.ItemText> - <Menu.ItemIcon icon={Share} position="right" /> - </Menu.Item> - <Menu.Item - testID="postAuthorDIDShareBtn" - label={_(msg`Copy author DID`)} - onPress={onShareAuthorDID}> - <Menu.ItemText>{_(msg`Copy author DID`)}</Menu.ItemText> - <Menu.ItemIcon icon={Share} position="right" /> - </Menu.Item> - </Menu.Group> - </> - ) : null} - </> - )} - </Menu.Outer> - - <Prompt.Basic - control={deletePromptControl} - title={_(msg`Delete this post?`)} - description={_( - msg`If you remove this post, you won't be able to recover it.`, - )} - onConfirm={onDeletePost} - confirmButtonCta={_(msg`Delete`)} - confirmButtonColor="negative" - /> - - <Prompt.Basic - control={hidePromptControl} - title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)} - description={_( - msg`This post will be hidden from feeds and threads. This cannot be undone.`, - )} - onConfirm={onHidePost} - confirmButtonCta={_(msg`Hide`)} - /> - - <ReportDialog - control={reportDialogControl} - subject={{ - ...post, - $type: 'app.bsky.feed.defs#postView', - }} - /> - - <Prompt.Basic - control={loggedOutWarningPromptControl} - title={_(msg`Note about sharing`)} - description={_( - msg`This post is only visible to logged-in users. It won't be visible to people who aren't signed in.`, - )} - onConfirm={onSharePost} - confirmButtonCta={_(msg`Share anyway`)} - /> - - {canEmbed && ( - <EmbedDialog - control={embedPostControl} - postCid={postCid} - postUri={postUri} - record={record} - postAuthor={postAuthor} - timestamp={timestamp} - /> - )} - - <SendViaChatDialog - control={sendViaChatControl} - onSelectChat={onSelectChatToShareTo} - /> - - <PostInteractionSettingsDialog - control={postInteractionSettingsDialogControl} - postUri={post.uri} - rootPostUri={rootUri} - initialThreadgateView={post.threadgate} - /> - - <Prompt.Basic - control={quotePostDetachConfirmControl} - title={_(msg`Detach quote post?`)} - description={_( - msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`, - )} - onConfirm={onToggleQuotePostAttachment} - confirmButtonCta={_(msg`Yes, detach`)} - /> - - <Prompt.Basic - control={hideReplyConfirmControl} - title={_(msg`Hide this reply?`)} - description={_( - msg`This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others.`, - )} - onConfirm={onToggleReplyVisibility} - confirmButtonCta={_(msg`Yes, hide`)} - /> - - <Prompt.Basic - control={blockPromptControl} - title={_(msg`Block Account?`)} - description={_( - msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, - )} - onConfirm={onBlockAuthor} - confirmButtonCta={_(msg`Block`)} - confirmButtonColor="negative" - /> - </> - ) -} -PostDropdownMenuItems = memo(PostDropdownMenuItems) -export {PostDropdownMenuItems} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx deleted file mode 100644 index 3f82eb294..000000000 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ /dev/null @@ -1,394 +0,0 @@ -import React, {memo} from 'react' -import { - Pressable, - type PressableStateCallbackType, - type StyleProp, - View, - type ViewStyle, -} from 'react-native' -import * as Clipboard from 'expo-clipboard' -import { - type AppBskyFeedDefs, - type AppBskyFeedPost, - type AppBskyFeedThreadgate, - AtUri, - type RichText as RichTextAPI, -} from '@atproto/api' -import {msg, plural} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {IS_INTERNAL} from '#/lib/app-info' -import {DISCOVER_DEBUG_DIDS, POST_CTRL_HITSLOP} from '#/lib/constants' -import {CountWheel} from '#/lib/custom-animations/CountWheel' -import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' -import {useHaptics} from '#/lib/haptics' -import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {makeProfileLink} from '#/lib/routes/links' -import {shareUrl} from '#/lib/sharing' -import {useGate} from '#/lib/statsig/statsig' -import {toShareUrl} from '#/lib/strings/url-helpers' -import {type Shadow} from '#/state/cache/types' -import {useFeedFeedbackContext} from '#/state/feed-feedback' -import { - usePostLikeMutationQueue, - usePostRepostMutationQueue, -} from '#/state/queries/post' -import {useRequireAuth, useSession} from '#/state/session' -import { - ProgressGuideAction, - useProgressGuideControls, -} from '#/state/shell/progress-guide' -import {atoms as a, useTheme} from '#/alf' -import {useDialogControl} from '#/components/Dialog' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' -import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' -import * as Prompt from '#/components/Prompt' -import {PostDropdownBtn} from '../forms/PostDropdownBtn' -import {formatCount} from '../numeric/format' -import {Text} from '../text/Text' -import * as Toast from '../Toast' -import {RepostButton} from './RepostButton' - -let PostCtrls = ({ - big, - post, - record, - richText, - feedContext, - reqId, - style, - onPressReply, - onPostReply, - logContext, - threadgateRecord, - onShowLess, -}: { - big?: boolean - post: Shadow<AppBskyFeedDefs.PostView> - record: AppBskyFeedPost.Record - richText: RichTextAPI - feedContext?: string | undefined - reqId?: string | undefined - style?: StyleProp<ViewStyle> - onPressReply: () => void - onPostReply?: (postUri: string | undefined) => void - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' - threadgateRecord?: AppBskyFeedThreadgate.Record - onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void -}): React.ReactNode => { - const t = useTheme() - const {_, i18n} = useLingui() - const {openComposer} = useOpenComposer() - const {currentAccount} = useSession() - const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) - const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( - post, - logContext, - ) - const requireAuth = useRequireAuth() - const loggedOutWarningPromptControl = useDialogControl() - const {sendInteraction} = useFeedFeedbackContext() - const {captureAction} = useProgressGuideControls() - const playHaptic = useHaptics() - const gate = useGate() - const isDiscoverDebugUser = - IS_INTERNAL || - DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] || - gate('debug_show_feedcontext') - const isBlocked = Boolean( - post.author.viewer?.blocking || - post.author.viewer?.blockedBy || - post.author.viewer?.blockingByList, - ) - const replyDisabled = post.viewer?.replyDisabled - - const shouldShowLoggedOutWarning = React.useMemo(() => { - return ( - post.author.did !== currentAccount?.did && - !!post.author.labels?.find(label => label.val === '!no-unauthenticated') - ) - }, [currentAccount, post]) - - const defaultCtrlColor = React.useMemo( - () => ({ - color: t.palette.contrast_500, - }), - [t], - ) as StyleProp<ViewStyle> - - const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = - React.useState(false) - - const onPressToggleLike = async () => { - if (isBlocked) { - Toast.show( - _(msg`Cannot interact with a blocked user`), - 'exclamation-circle', - ) - return - } - - try { - setHasLikeIconBeenToggled(true) - if (!post.viewer?.like) { - playHaptic('Light') - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionLike', - feedContext, - reqId, - }) - captureAction(ProgressGuideAction.Like) - await queueLike() - } else { - await queueUnlike() - } - } catch (e: any) { - if (e?.name !== 'AbortError') { - throw e - } - } - } - - const onRepost = async () => { - if (isBlocked) { - Toast.show( - _(msg`Cannot interact with a blocked user`), - 'exclamation-circle', - ) - return - } - - try { - if (!post.viewer?.repost) { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionRepost', - feedContext, - reqId, - }) - await queueRepost() - } else { - await queueUnrepost() - } - } catch (e: any) { - if (e?.name !== 'AbortError') { - throw e - } - } - } - - const onQuote = () => { - if (isBlocked) { - Toast.show( - _(msg`Cannot interact with a blocked user`), - 'exclamation-circle', - ) - return - } - - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionQuote', - feedContext, - reqId, - }) - openComposer({ - quote: post, - onPost: onPostReply, - }) - } - - const onShare = () => { - const urip = new AtUri(post.uri) - const href = makeProfileLink(post.author, 'post', urip.rkey) - const url = toShareUrl(href) - shareUrl(url) - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionShare', - feedContext, - reqId, - }) - } - - const btnStyle = React.useCallback( - ({pressed, hovered}: PressableStateCallbackType) => [ - a.gap_xs, - a.rounded_full, - a.flex_row, - a.justify_center, - a.align_center, - a.overflow_hidden, - {padding: 5}, - (pressed || hovered) && t.atoms.bg_contrast_25, - ], - [t.atoms.bg_contrast_25], - ) - - return ( - <View style={[a.flex_row, a.justify_between, a.align_center, style]}> - <View - style={[ - big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}], - replyDisabled ? {opacity: 0.5} : undefined, - ]}> - <Pressable - testID="replyBtn" - style={btnStyle} - onPress={() => { - if (!replyDisabled) { - playHaptic('Light') - requireAuth(() => onPressReply()) - } - }} - accessibilityRole="button" - accessibilityLabel={_( - msg`Reply (${plural(post.replyCount || 0, { - one: '# reply', - other: '# replies', - })})`, - )} - accessibilityHint="" - hitSlop={POST_CTRL_HITSLOP}> - <Bubble - style={[defaultCtrlColor, {pointerEvents: 'none'}]} - width={big ? 22 : 18} - /> - {typeof post.replyCount !== 'undefined' && post.replyCount > 0 ? ( - <Text - style={[ - defaultCtrlColor, - big ? a.text_md : {fontSize: 15}, - a.user_select_none, - ]}> - {formatCount(i18n, post.replyCount)} - </Text> - ) : undefined} - </Pressable> - </View> - <View style={big ? a.align_center : [a.flex_1, a.align_start]}> - <RepostButton - isReposted={!!post.viewer?.repost} - repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)} - onRepost={onRepost} - onQuote={onQuote} - big={big} - embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} - /> - </View> - <View style={big ? a.align_center : [a.flex_1, a.align_start]}> - <Pressable - testID="likeBtn" - style={btnStyle} - onPress={() => requireAuth(() => onPressToggleLike())} - accessibilityRole="button" - accessibilityLabel={ - post.viewer?.like - ? _( - msg`Unlike (${plural(post.likeCount || 0, { - one: '# like', - other: '# likes', - })})`, - ) - : _( - msg`Like (${plural(post.likeCount || 0, { - one: '# like', - other: '# likes', - })})`, - ) - } - accessibilityHint="" - hitSlop={POST_CTRL_HITSLOP}> - <AnimatedLikeIcon - isLiked={Boolean(post.viewer?.like)} - big={big} - hasBeenToggled={hasLikeIconBeenToggled} - /> - <CountWheel - likeCount={post.likeCount ?? 0} - big={big} - isLiked={Boolean(post.viewer?.like)} - hasBeenToggled={hasLikeIconBeenToggled} - /> - </Pressable> - </View> - {big && ( - <> - <View style={a.align_center}> - <Pressable - testID="shareBtn" - style={btnStyle} - onPress={() => { - if (shouldShowLoggedOutWarning) { - loggedOutWarningPromptControl.open() - } else { - onShare() - } - }} - accessibilityRole="button" - accessibilityLabel={_(msg`Share`)} - accessibilityHint="" - hitSlop={POST_CTRL_HITSLOP}> - <ArrowOutOfBox - style={[defaultCtrlColor, {pointerEvents: 'none'}]} - width={22} - /> - </Pressable> - </View> - <Prompt.Basic - control={loggedOutWarningPromptControl} - title={_(msg`Note about sharing`)} - description={_( - msg`This post is only visible to logged-in users. It won't be visible to people who aren't signed in.`, - )} - onConfirm={onShare} - confirmButtonCta={_(msg`Share anyway`)} - /> - </> - )} - <View style={big ? a.align_center : [a.flex_1, a.align_start]}> - <PostDropdownBtn - testID="postDropdownBtn" - post={post} - postFeedContext={feedContext} - postReqId={reqId} - record={record} - richText={richText} - style={{padding: 5}} - hitSlop={POST_CTRL_HITSLOP} - timestamp={post.indexedAt} - threadgateRecord={threadgateRecord} - onShowLess={onShowLess} - /> - </View> - {isDiscoverDebugUser && feedContext && ( - <Pressable - accessible={false} - style={{ - position: 'absolute', - top: 0, - bottom: 0, - right: 0, - display: 'flex', - justifyContent: 'center', - }} - onPress={e => { - e.stopPropagation() - Clipboard.setStringAsync(feedContext) - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') - }}> - <Text - style={{ - color: t.palette.contrast_400, - fontSize: 7, - }}> - {feedContext} - </Text> - </Pressable> - )} - </View> - ) -} -PostCtrls = memo(PostCtrls) -export {PostCtrls} diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx deleted file mode 100644 index ca1647a99..000000000 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React, {memo, useCallback} from 'react' -import {View} from 'react-native' -import {msg, plural, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {POST_CTRL_HITSLOP} from '#/lib/constants' -import {useHaptics} from '#/lib/haptics' -import {useRequireAuth} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote' -import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' -import {Text} from '#/components/Typography' -import {formatCount} from '../numeric/format' - -interface Props { - isReposted: boolean - repostCount?: number - onRepost: () => void - onQuote: () => void - big?: boolean - embeddingDisabled: boolean -} - -let RepostButton = ({ - isReposted, - repostCount, - onRepost, - onQuote, - big, - embeddingDisabled, -}: Props): React.ReactNode => { - const t = useTheme() - const {_, i18n} = useLingui() - const requireAuth = useRequireAuth() - const dialogControl = Dialog.useDialogControl() - const playHaptic = useHaptics() - const color = React.useMemo( - () => ({ - color: isReposted ? t.palette.positive_600 : t.palette.contrast_500, - }), - [t, isReposted], - ) - return ( - <> - <Button - testID="repostBtn" - onPress={() => { - playHaptic('Light') - requireAuth(() => dialogControl.open()) - }} - onLongPress={() => { - playHaptic('Heavy') - requireAuth(() => onQuote()) - }} - style={[ - a.flex_row, - a.align_center, - a.gap_xs, - a.bg_transparent, - {padding: 5}, - ]} - hoverStyle={t.atoms.bg_contrast_25} - label={ - isReposted - ? _( - msg`Undo repost (${plural(repostCount || 0, { - one: '# repost', - other: '# reposts', - })})`, - ) - : _( - msg`Repost (${plural(repostCount || 0, { - one: '# repost', - other: '# reposts', - })})`, - ) - } - shape="round" - variant="ghost" - color="secondary" - hitSlop={POST_CTRL_HITSLOP}> - <Repost style={color} width={big ? 22 : 18} /> - {typeof repostCount !== 'undefined' && repostCount > 0 ? ( - <Text - testID="repostCount" - style={[ - color, - big ? a.text_md : {fontSize: 15}, - isReposted && a.font_bold, - ]}> - {formatCount(i18n, repostCount)} - </Text> - ) : undefined} - </Button> - <Dialog.Outer - control={dialogControl} - nativeOptions={{preventExpansion: true}}> - <Dialog.Handle /> - <RepostButtonDialogInner - isReposted={isReposted} - onRepost={onRepost} - onQuote={onQuote} - embeddingDisabled={embeddingDisabled} - /> - </Dialog.Outer> - </> - ) -} -RepostButton = memo(RepostButton) -export {RepostButton} - -let RepostButtonDialogInner = ({ - isReposted, - onRepost, - onQuote, - embeddingDisabled, -}: { - isReposted: boolean - onRepost: () => void - onQuote: () => void - embeddingDisabled: boolean -}): React.ReactNode => { - const t = useTheme() - const {_} = useLingui() - const playHaptic = useHaptics() - const control = Dialog.useDialogContext() - - const onPressRepost = useCallback(() => { - if (!isReposted) playHaptic() - - control.close(() => { - onRepost() - }) - }, [control, isReposted, onRepost, playHaptic]) - - const onPressQuote = useCallback(() => { - playHaptic() - control.close(() => { - onQuote() - }) - }, [control, onQuote, playHaptic]) - - const onPressClose = useCallback(() => control.close(), [control]) - - return ( - <Dialog.ScrollableInner label={_(msg`Repost or quote post`)}> - <View style={a.gap_xl}> - <View style={a.gap_xs}> - <Button - style={[a.justify_start, a.px_md]} - label={ - isReposted - ? _(msg`Remove repost`) - : _(msg({message: `Repost`, context: 'action'})) - } - onPress={onPressRepost} - size="large" - variant="ghost" - color="primary"> - <Repost size="lg" fill={t.palette.primary_500} /> - <Text style={[a.font_bold, a.text_xl]}> - {isReposted ? ( - <Trans>Remove repost</Trans> - ) : ( - <Trans context="action">Repost</Trans> - )} - </Text> - </Button> - <Button - disabled={embeddingDisabled} - testID="quoteBtn" - style={[a.justify_start, a.px_md]} - label={ - embeddingDisabled - ? _(msg`Quote posts disabled`) - : _(msg`Quote post`) - } - onPress={onPressQuote} - size="large" - variant="ghost" - color="primary"> - <Quote - size="lg" - fill={ - embeddingDisabled - ? t.atoms.text_contrast_low.color - : t.palette.primary_500 - } - /> - <Text - style={[ - a.font_bold, - a.text_xl, - embeddingDisabled && t.atoms.text_contrast_low, - ]}> - {embeddingDisabled ? ( - <Trans>Quote posts disabled</Trans> - ) : ( - <Trans>Quote post</Trans> - )} - </Text> - </Button> - </View> - <Button - label={_(msg`Cancel quote post`)} - onPress={onPressClose} - size="large" - variant="outline" - color="primary"> - <ButtonText> - <Trans>Cancel</Trans> - </ButtonText> - </Button> - </View> - </Dialog.ScrollableInner> - ) -} -RepostButtonDialogInner = memo(RepostButtonDialogInner) -export {RepostButtonDialogInner} diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx deleted file mode 100644 index 54119b532..000000000 --- a/src/view/com/util/post-ctrls/RepostButton.web.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react' -import {Pressable, View} from 'react-native' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useRequireAuth} from '#/state/session' -import {useSession} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' -import {Button} from '#/components/Button' -import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote' -import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' -import * as Menu from '#/components/Menu' -import {Text} from '#/components/Typography' -import {EventStopper} from '../EventStopper' -import {formatCount} from '../numeric/format' - -interface Props { - isReposted: boolean - repostCount?: number - onRepost: () => void - onQuote: () => void - big?: boolean - embeddingDisabled: boolean -} - -export const RepostButton = ({ - isReposted, - repostCount, - onRepost, - onQuote, - big, - embeddingDisabled, -}: Props) => { - const t = useTheme() - const {_} = useLingui() - const {hasSession} = useSession() - const requireAuth = useRequireAuth() - - const color = React.useMemo( - () => ({ - color: isReposted ? t.palette.positive_600 : t.palette.contrast_500, - }), - [t, isReposted], - ) - - return hasSession ? ( - <EventStopper onKeyDown={false}> - <Menu.Root> - <Menu.Trigger label={_(msg`Repost or quote post`)}> - {({props, state}) => { - return ( - <Pressable - {...props} - style={[ - a.rounded_full, - (state.hovered || state.pressed) && { - backgroundColor: t.palette.contrast_25, - }, - ]}> - <RepostInner - isReposted={isReposted} - color={color} - repostCount={repostCount} - big={big} - /> - </Pressable> - ) - }} - </Menu.Trigger> - <Menu.Outer style={{minWidth: 170}}> - <Menu.Item - label={isReposted ? _(msg`Undo repost`) : _(msg`Repost`)} - testID="repostDropdownRepostBtn" - onPress={onRepost}> - <Menu.ItemText> - {isReposted ? _(msg`Undo repost`) : _(msg`Repost`)} - </Menu.ItemText> - <Menu.ItemIcon icon={Repost} position="right" /> - </Menu.Item> - <Menu.Item - disabled={embeddingDisabled} - label={ - embeddingDisabled - ? _(msg`Quote posts disabled`) - : _(msg`Quote post`) - } - testID="repostDropdownQuoteBtn" - onPress={onQuote}> - <Menu.ItemText> - {embeddingDisabled - ? _(msg`Quote posts disabled`) - : _(msg`Quote post`)} - </Menu.ItemText> - <Menu.ItemIcon icon={Quote} position="right" /> - </Menu.Item> - </Menu.Outer> - </Menu.Root> - </EventStopper> - ) : ( - <Button - onPress={() => { - requireAuth(() => {}) - }} - label={_(msg`Repost or quote post`)} - style={{padding: 0}} - hoverStyle={t.atoms.bg_contrast_25} - shape="round"> - <RepostInner - isReposted={isReposted} - color={color} - repostCount={repostCount} - big={big} - /> - </Button> - ) -} - -const RepostInner = ({ - isReposted, - color, - repostCount, - big, -}: { - isReposted: boolean - color: {color: string} - repostCount?: number - big?: boolean -}) => { - const {i18n} = useLingui() - return ( - <View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}> - <Repost style={color} width={big ? 22 : 18} /> - {typeof repostCount !== 'undefined' && repostCount > 0 ? ( - <Text - testID="repostCount" - style={[ - color, - big ? a.text_md : {fontSize: 15}, - isReposted && [a.font_bold], - a.user_select_none, - ]}> - {formatCount(i18n, repostCount)} - </Text> - ) : undefined} - </View> - ) -} |