diff options
Diffstat (limited to 'src/view/com/util/post-ctrls')
-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 |
3 files changed, 0 insertions, 762 deletions
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> - ) -} |