Compare commits

...

20 Commits

Author SHA1 Message Date
Samuel Newman 52a8794940 add a/b tested alt icon 2025-05-22 12:16:06 +03:00
Samuel Newman 7ca9bd0e80 add metrics 2025-05-22 12:06:18 +03:00
Samuel Newman 4a301753e2 tweak scrollview 2025-05-22 12:02:51 +03:00
Samuel Newman e21bee6e35 add fading edge on andriod 2025-05-22 12:02:51 +03:00
Samuel Newman 120ed34ee2 Update RecentChats.tsx 2025-05-22 12:02:51 +03:00
Samuel Newman c79707bdc3 Update RecentChats.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2025-05-22 12:02:51 +03:00
Samuel Newman 842915db64 add recent chats to share menu 2025-05-22 12:02:51 +03:00
Samuel Newman e7fc174266 add better icon 2025-05-22 11:53:27 +03:00
Samuel Newman c594461fe8 equal spacing of buttons, extract disco debug 2025-05-22 11:53:27 +03:00
Samuel Newman 4a69e4fe40 rm react import 2025-05-22 11:52:49 +03:00
Samuel Newman b1727842cf update type imports, remove forwardRef 2025-05-22 11:52:29 +03:00
Samuel Newman 5ad62b8ce4 abs path 2025-05-22 11:51:57 +03:00
Samuel Newman b94371e960 Translation comments
Thanks @surfdude29
2025-05-22 11:51:57 +03:00
Samuel Newman 4ae248eb1e open native share menu on long press 2025-05-22 11:51:57 +03:00
Samuel Newman cd245a3e26 reorg files again 2025-05-22 11:51:20 +03:00
Samuel Newman 17cf05c5f3 add copy link to native share menu 2025-05-22 11:51:00 +03:00
Samuel Newman 524510a361 Revert "add border radius to searchable people list for android"
This reverts commit 417449086e25b82f5683b12f6405d972f48ce50e.
2025-05-22 11:48:24 +03:00
Samuel Newman bc17c830cc add border radius to searchable people list for android 2025-05-22 11:48:24 +03:00
Samuel Newman bb2c6fe2d1 restructure post controls, basic share menu 2025-05-22 11:48:24 +03:00
Samuel Newman 5c5e2a3189 move post ctrls to #/components 2025-05-22 11:38:19 +03:00
27 changed files with 1173 additions and 505 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M20 13.75a1 1 0 0 1 1 1V18a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3v-3.25a1 1 0 1 1 2 0V18a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.25a1 1 0 0 1 1-1ZM12 3a1 1 0 0 1 .707.293l4.5 4.5a1 1 0 1 1-1.414 1.414L13 6.414v8.836a1 1 0 1 1-2 0V6.414L8.207 9.207a1 1 0 1 1-1.414-1.414l4.5-4.5A1 1 0 0 1 12 3Z"/></svg>

After

Width:  |  Height:  |  Size: 381 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M11.839 4.744c0-1.488 1.724-2.277 2.846-1.364l.107.094 7.66 7.256.128.134c.558.652.558 1.62 0 2.272l-.128.135-7.66 7.255c-1.115 1.057-2.953.267-2.953-1.27v-2.748c-3.503.055-5.417.41-6.592.97-.997.474-1.525 1.122-2.084 2.14l-.243.46c-.558 1.088-2.09.583-2.08-.515l.015-.748c.111-3.68.777-6.5 2.546-8.415 1.83-1.98 4.63-2.771 8.438-2.884V4.744Zm2 3.256c0 .79-.604 1.41-1.341 1.494l-.149.01c-3.9.057-6.147.813-7.48 2.254-.963 1.043-1.562 2.566-1.842 4.79.38-.327.826-.622 1.361-.877 1.656-.788 4.08-1.14 7.938-1.169l.153.007A1.5 1.5 0 0 1 13.839 16v2.675L20.884 12l-7.045-6.676V8Z"/></svg>

After

Width:  |  Height:  |  Size: 680 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 610 B

+35 -2
View File
@@ -244,6 +244,38 @@ export function ItemRadio({selected}: {selected: boolean}) {
)
}
/**
* NATIVE ONLY - for adding non-pressable items to the menu
*
* @platform ios, android
*/
export function ContainerItem({
children,
style,
}: {
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
const t = useTheme()
return (
<View
style={[
a.flex_row,
a.align_center,
a.gap_sm,
a.px_md,
a.rounded_md,
a.border,
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
{paddingVertical: 10},
style,
]}>
{children}
</View>
)
}
export function LabelText({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
@@ -272,13 +304,14 @@ export function Group({children, style}: GroupProps) {
style,
]}>
{flattenReactChildren(children).map((child, i) => {
return React.isValidElement(child) && child.type === Item ? (
return React.isValidElement(child) &&
(child.type === Item || child.type === ContainerItem) ? (
<React.Fragment key={i}>
{i > 0 ? (
<View style={[a.border_b, t.atoms.border_contrast_low]} />
) : null}
{React.cloneElement(child, {
// @ts-ignore
// @ts-expect-error cloneElement is not aware of the types
style: {
borderRadius: 0,
borderWidth: 0,
+4
View File
@@ -390,3 +390,7 @@ export function Divider() {
/>
)
}
export function ContainerItem() {
return null
}
@@ -0,0 +1,54 @@
import {Pressable} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {t} from '@lingui/macro'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
export function DiscoverDebug({
feedContext,
}: {
feedContext: string | undefined
}) {
const {currentAccount} = useSession()
const {gtMobile} = useBreakpoints()
const gate = useGate()
const isDiscoverDebugUser =
IS_INTERNAL ||
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
gate('debug_show_feedcontext')
const theme = useTheme()
return (
isDiscoverDebugUser &&
feedContext && (
<Pressable
accessible={false}
hitSlop={10}
style={[
a.absolute,
a.bottom_0,
{zIndex: 1000},
gtMobile ? a.right_0 : a.left_0,
]}
onPress={e => {
e.stopPropagation()
Clipboard.setStringAsync(feedContext)
Toast.show(t`Copied to clipboard`, 'clipboard-check')
}}>
<Text
style={{
color: theme.palette.contrast_400,
fontSize: 7,
}}>
{feedContext}
</Text>
</Pressable>
)
)
}
@@ -0,0 +1,126 @@
import {createContext, useContext, useMemo} from 'react'
import {type GestureResponderEvent, type View} from 'react-native'
import {POST_CTRL_HITSLOP} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {atoms as a, useTheme} from '#/alf'
import {Button, type ButtonProps} from '#/components/Button'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text, type TextProps} from '#/components/Typography'
const PostControlContext = createContext<{
big?: boolean
active?: boolean
color?: {color: string}
}>({})
// Base button style, which the the other ones extend
export function PostControlButton({
ref,
onPress,
onLongPress,
children,
big,
active,
activeColor,
...props
}: ButtonProps & {
ref?: React.Ref<View>
active?: boolean
big?: boolean
color?: string
activeColor?: string
}) {
const t = useTheme()
const playHaptic = useHaptics()
const ctx = useMemo(
() => ({
big,
active,
color: {
color: activeColor && active ? activeColor : t.palette.contrast_500,
},
}),
[big, active, activeColor, t.palette.contrast_500],
)
const style = useMemo(
() => [
a.flex_row,
a.align_center,
a.gap_xs,
a.bg_transparent,
{padding: 5},
],
[],
)
const handlePress = useMemo(() => {
if (!onPress) return
return (evt: GestureResponderEvent) => {
playHaptic('Light')
onPress(evt)
}
}, [onPress, playHaptic])
const handleLongPress = useMemo(() => {
if (!onLongPress) return
return (evt: GestureResponderEvent) => {
playHaptic('Heavy')
onLongPress(evt)
}
}, [onLongPress, playHaptic])
return (
<Button
ref={ref}
onPress={handlePress}
onLongPress={handleLongPress}
style={style}
hoverStyle={t.atoms.bg_contrast_25}
shape="round"
variant="ghost"
color="secondary"
hitSlop={POST_CTRL_HITSLOP}
{...props}>
{typeof children === 'function' ? (
args => (
<PostControlContext.Provider value={ctx}>
{children(args)}
</PostControlContext.Provider>
)
) : (
<PostControlContext.Provider value={ctx}>
{children}
</PostControlContext.Provider>
)}
</Button>
)
}
export function PostControlButtonIcon({
icon: Comp,
}: {
icon: React.ComponentType<SVGIconProps>
}) {
const {big, color} = useContext(PostControlContext)
return <Comp style={[color, a.pointer_events_none]} width={big ? 22 : 18} />
}
export function PostControlButtonText({style, ...props}: TextProps) {
const {big, active, color} = useContext(PostControlContext)
return (
<Text
style={[
color,
big ? a.text_md : {fontSize: 15},
active && a.font_bold,
style,
]}
{...props}
/>
)
}
@@ -1,4 +1,4 @@
import React, {memo} from 'react'
import {memo, useMemo} from 'react'
import {
Platform,
type PressableProps,
@@ -13,7 +13,7 @@ import {
AtUri,
type RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -26,13 +26,11 @@ 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'
@@ -52,20 +50,16 @@ import {
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
import {useSession} from '#/state/session'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {useBreakpoints} from '#/alf'
import * as Toast from '#/view/com/util/Toast'
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,
@@ -75,7 +69,6 @@ import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/E
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'
@@ -90,17 +83,14 @@ import {
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 = ({
let PostMenuItems = ({
post,
postFeedContext,
postReqId,
record,
richText,
timestamp,
threadgateRecord,
onShowLess,
}: {
@@ -118,7 +108,6 @@ let PostDropdownMenuItems = ({
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
@@ -134,20 +123,16 @@ let PostDropdownMenuItems = ({
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(() => {
const quoteEmbed = useMemo(() => {
if (!currentAccount || !post.embed) return
return getMaybeDetachedQuoteEmbed({
viewerDid: currentAccount.did,
@@ -181,7 +166,7 @@ let PostDropdownMenuItems = ({
rootPostUri: rootUri,
})
const href = React.useMemo(() => {
const href = useMemo(() => {
const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey)
}, [postUri, postAuthor])
@@ -273,14 +258,6 @@ let PostDropdownMenuItems = ({
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',
@@ -308,13 +285,6 @@ let PostDropdownMenuItems = ({
}
}
const onSelectChatToShareTo = (conversation: string) => {
navigation.navigate('MessagesConversation', {
conversation,
embed: postUri,
})
}
const onToggleQuotePostAttachment = async () => {
if (!quoteEmbed) return
@@ -341,7 +311,6 @@ let PostDropdownMenuItems = ({
}
const canHidePostForMe = !isAuthor && !isPostHidden
const canEmbed = isWeb && gtMobile && !hideInPWI
const canHideReplyForEveryone =
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
@@ -417,14 +386,6 @@ let PostDropdownMenuItems = ({
}
}
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,
@@ -482,44 +443,6 @@ let PostDropdownMenuItems = ({
</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 && (
@@ -550,11 +473,9 @@ let PostDropdownMenuItems = ({
DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<Menu.Item
testID="postDropdownReportMisclassificationBtn"
label={_(msg`Assign topic - help train Discover!`)}
label={_(msg`Assign topic for algo`)}
onPress={onReportMisclassification}>
<Menu.ItemText>
{_(msg`Assign topic - help train Discover!`)}
</Menu.ItemText>
<Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
<Menu.ItemIcon icon={AtomIcon} position="right" />
</Menu.Item>
)}
@@ -747,28 +668,6 @@ let PostDropdownMenuItems = ({
</>
)}
</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>
@@ -802,32 +701,6 @@ let PostDropdownMenuItems = ({
}}
/>
<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}
@@ -868,5 +741,5 @@ let PostDropdownMenuItems = ({
</>
)
}
PostDropdownMenuItems = memo(PostDropdownMenuItems)
export {PostDropdownMenuItems}
PostMenuItems = memo(PostMenuItems)
export {PostMenuItems}
@@ -1,10 +1,4 @@
import {memo, useMemo, useState} from 'react'
import {
Pressable,
type PressableProps,
type StyleProp,
type ViewStyle,
} from 'react-native'
import {
type AppBskyFeedDefs,
type AppBskyFeedPost,
@@ -15,25 +9,22 @@ 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 {EventStopper} from '#/view/com/util/EventStopper'
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'
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
import {PostMenuItems} from './PostMenuItems'
let PostDropdownBtn = ({
let PostMenuButton = ({
testID,
post,
postFeedContext,
postReqId,
big,
record,
richText,
style,
hitSlop,
size,
timestamp,
threadgateRecord,
onShowLess,
@@ -42,19 +33,15 @@ let PostDropdownBtn = ({
post: Shadow<AppBskyFeedDefs.PostView>
postFeedContext: string | undefined
postReqId: string | undefined
big?: boolean
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(
@@ -73,31 +60,21 @@ let PostDropdownBtn = ({
<EventStopper onKeyDown={false}>
<Menu.Root control={lazyMenuControl}>
<Menu.Trigger label={_(msg`Open post options menu`)}>
{({props, state}) => {
{({props}) => {
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>
<PostControlButton
testID="postDropdownBtn"
big={big}
label={props.accessibilityLabel}
{...props}>
<PostControlButtonIcon icon={DotsHorizontal} />
</PostControlButton>
)
}}
</Menu.Trigger>
{hasBeenOpen && (
// Lazily initialized. Once mounted, they stay mounted.
<PostDropdownMenuItems
<PostMenuItems
testID={testID}
post={post}
postFeedContext={postFeedContext}
@@ -114,5 +91,5 @@ let PostDropdownBtn = ({
)
}
PostDropdownBtn = memo(PostDropdownBtn)
export {PostDropdownBtn}
PostMenuButton = memo(PostMenuButton)
export {PostMenuButton}
@@ -1,18 +1,22 @@
import React, {memo, useCallback} from 'react'
import {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 {formatCount} from '#/view/com/util/numeric/format'
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'
import {
PostControlButton,
PostControlButtonIcon,
PostControlButtonText,
} from './PostControlButton'
interface Props {
isReposted: boolean
@@ -35,65 +39,46 @@ let RepostButton = ({
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
<PostControlButton
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}
active={isReposted}
activeColor={t.palette.positive_600}
big={big}
onPress={() => requireAuth(() => dialogControl.open())}
onLongPress={() => requireAuth(() => onQuote())}
label={
isReposted
? _(
msg`Undo repost (${plural(repostCount || 0, {
one: '# repost',
other: '# reposts',
})})`,
msg({
message: `Undo repost (${plural(repostCount || 0, {
one: '# repost',
other: '# reposts',
})})`,
comment:
'Accessibility label for the repost button when the post has been reposted, verb followed by number of reposts and noun',
}),
)
: _(
msg`Repost (${plural(repostCount || 0, {
one: '# repost',
other: '# reposts',
})})`,
msg({
message: `Repost (${plural(repostCount || 0, {
one: '# repost',
other: '# reposts',
})})`,
comment:
'Accessibility label for the repost button when the post has not been reposted, verb form followed by number of reposts and noun form',
}),
)
}
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,
]}>
}>
<PostControlButtonIcon icon={Repost} />
{typeof repostCount !== 'undefined' && repostCount > 0 && (
<PostControlButtonText testID="repostCount">
{formatCount(i18n, repostCount)}
</Text>
) : undefined}
</Button>
</PostControlButtonText>
)}
</PostControlButton>
<Dialog.Outer
control={dialogControl}
nativeOptions={{preventExpansion: true}}>
@@ -1,18 +1,19 @@
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 {EventStopper} from '#/view/com/util/EventStopper'
import {formatCount} from '#/view/com/util/numeric/format'
import {useTheme} from '#/alf'
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'
import {
PostControlButton,
PostControlButtonIcon,
PostControlButtonText,
} from './PostControlButton'
interface Props {
isReposted: boolean
@@ -32,38 +33,30 @@ export const RepostButton = ({
embeddingDisabled,
}: Props) => {
const t = useTheme()
const {_} = useLingui()
const {_, i18n} = 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}) => {
{({props}) => {
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>
<PostControlButton
testID="repostBtn"
active={isReposted}
activeColor={t.palette.positive_600}
label={props.accessibilityLabel}
big={big}
{...props}>
<PostControlButtonIcon icon={Repost} />
{typeof repostCount !== 'undefined' && repostCount > 0 && (
<PostControlButtonText testID="repostCount">
{formatCount(i18n, repostCount)}
</PostControlButtonText>
)}
</PostControlButton>
)
}}
</Menu.Trigger>
@@ -97,51 +90,18 @@ export const RepostButton = ({
</Menu.Root>
</EventStopper>
) : (
<Button
onPress={() => {
requireAuth(() => {})
}}
<PostControlButton
onPress={() => requireAuth(() => {})}
active={isReposted}
activeColor={t.palette.positive_600}
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,
]}>
big={big}>
<PostControlButtonIcon icon={Repost} />
{typeof repostCount !== 'undefined' && repostCount > 0 && (
<PostControlButtonText testID="repostCount">
{formatCount(i18n, repostCount)}
</Text>
) : undefined}
</View>
</PostControlButtonText>
)}
</PostControlButton>
)
}
@@ -0,0 +1,200 @@
import {ScrollView, View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useDialogContext} from '#/components/Dialog'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import type * as bsky from '#/types/bsky'
export function RecentChats({postUri}: {postUri: string}) {
const control = useDialogContext()
const {_} = useLingui()
const {currentAccount} = useSession()
const {data} = useListConvosQuery({status: 'accepted'})
const convos = data?.pages[0]?.convos?.slice(0, 10)
const moderationOpts = useModerationOpts()
const navigation = useNavigation<NavigationProp>()
const onSelectChat = (convoId: string) => {
control.close(() => {
logger.metric('share:press:recentDm', {}, {statsig: true})
navigation.navigate('MessagesConversation', {
conversation: convoId,
embed: postUri,
})
})
}
if (!moderationOpts) return null
return (
<View
style={[a.relative, a.flex_1, {marginHorizontal: tokens.space.md * -1}]}>
<ScrollView
horizontal
style={[a.flex_1, a.pt_2xs, {minHeight: 98}]}
contentContainerStyle={[a.gap_sm, a.px_md]}
showsHorizontalScrollIndicator={false}
fadingEdgeLength={64}
nestedScrollEnabled>
{convos && convos.length > 0 ? (
convos.map(convo => {
const otherMember = convo.members.find(
member => member.did !== currentAccount?.did,
)
if (!otherMember || otherMember.handle === 'missing.invalid')
return null
return (
<RecentChatItem
key={convo.id}
profile={otherMember}
onPress={() => onSelectChat(convo.id)}
moderationOpts={moderationOpts}
/>
)
})
) : (
<>
<ConvoSkeleton />
<ConvoSkeleton />
<ConvoSkeleton />
<ConvoSkeleton />
<ConvoSkeleton />
</>
)}
</ScrollView>
{convos && convos.length === 0 && <NoConvos />}
</View>
)
}
const WIDTH = 80
function RecentChatItem({
profile,
onPress,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
onPress: () => void
moderationOpts: ModerationOpts
}) {
const {_} = useLingui()
const t = useTheme()
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const verification = useSimpleVerificationState({profile})
return (
<Button
onPress={onPress}
label={_(msg`Send post to ${name}`)}
style={[
a.flex_col,
{width: WIDTH},
a.gap_sm,
a.justify_start,
a.align_center,
]}>
<UserAvatar
avatar={profile.avatar}
size={WIDTH - 8}
type={profile.associated?.labeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')}
/>
<View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
<Text
emoji
style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{name}
</Text>
{verification.showBadge && (
<View style={[a.pl_2xs]}>
<VerificationCheck
width={10}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View>
</Button>
)
}
function ConvoSkeleton() {
const t = useTheme()
return (
<View
style={[
a.flex_col,
{width: WIDTH, height: WIDTH + 15},
a.gap_xs,
a.justify_start,
a.align_center,
]}>
<View
style={[
t.atoms.bg_contrast_50,
{width: WIDTH - 8, height: WIDTH - 8},
a.rounded_full,
]}
/>
<View
style={[
t.atoms.bg_contrast_50,
{width: WIDTH - 8, height: 10},
a.rounded_xs,
]}
/>
</View>
)
}
function NoConvos() {
const t = useTheme()
return (
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.align_center,
a.px_2xl,
]}>
<View
style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]}
/>
<Text
style={[
a.text_sm,
t.atoms.text_contrast_high,
a.text_center,
a.font_bold,
]}>
<Trans>Start a conversation, and it will appear here.</Trans>
</Text>
</View>
)
}
@@ -0,0 +1,197 @@
import {memo, useMemo} from 'react'
import * as ExpoClipboard from 'expo-clipboard'
import {AtUri} 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 {type NavigationProp} from '#/lib/routes/types'
import {shareText, shareUrl} from '#/lib/sharing'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {logger} from '#/logger'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useDialogControl} from '#/components/Dialog'
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {useDevMode} from '#/storage/hooks/dev-mode'
import {RecentChats} from './RecentChats'
import {type ShareMenuItemsProps} from './ShareMenuItems.types'
let ShareMenuItems = ({
post,
onShare: onShareProp,
}: ShareMenuItemsProps): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const pwiWarningShareControl = useDialogControl()
const pwiWarningCopyControl = useDialogControl()
const sendViaChatControl = useDialogControl()
const [devModeEnabled] = useDevMode()
const postUri = post.uri
const postAuthor = useProfileShadow(post.author)
const href = useMemo(() => {
const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey)
}, [postUri, postAuthor])
const hideInPWI = useMemo(() => {
return !!postAuthor.labels?.find(
label => label.val === '!no-unauthenticated',
)
}, [postAuthor])
const showLoggedOutWarning =
postAuthor.did !== currentAccount?.did && hideInPWI
const onSharePost = () => {
logger.metric('share:press:nativeShare', {}, {statsig: true})
const url = toShareUrl(href)
shareUrl(url)
onShareProp()
}
const onCopyLink = () => {
logger.metric('share:press:copyLink', {}, {statsig: true})
const url = toShareUrl(href)
ExpoClipboard.setUrlAsync(url).then(() =>
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check'),
)
onShareProp()
}
const onSelectChatToShareTo = (conversation: string) => {
navigation.navigate('MessagesConversation', {
conversation,
embed: postUri,
})
}
const onShareATURI = () => {
shareText(postUri)
}
const onShareAuthorDID = () => {
shareText(postAuthor.did)
}
return (
<>
<Menu.Outer>
{hasSession && (
<Menu.Group>
<Menu.ContainerItem>
<RecentChats postUri={postUri} />
</Menu.ContainerItem>
<Menu.Item
testID="postDropdownSendViaDMBtn"
label={_(msg`Send via direct message`)}
onPress={() => {
logger.metric('share:press:openDmSearch', {}, {statsig: true})
sendViaChatControl.open()
}}>
<Menu.ItemText>
<Trans>Send via direct message</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Send} position="right" />
</Menu.Item>
</Menu.Group>
)}
<Menu.Group>
<Menu.Item
testID="postDropdownShareBtn"
label={_(msg`Share via...`)}
onPress={() => {
if (showLoggedOutWarning) {
pwiWarningShareControl.open()
} else {
onSharePost()
}
}}>
<Menu.ItemText>
<Trans>Share via...</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" />
</Menu.Item>
<Menu.Item
testID="postDropdownShareBtn"
label={_(msg`Copy link to post`)}
onPress={() => {
if (showLoggedOutWarning) {
pwiWarningCopyControl.open()
} else {
onCopyLink()
}
}}>
<Menu.ItemText>
<Trans>Copy link to post</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ChainLinkIcon} position="right" />
</Menu.Item>
</Menu.Group>
{devModeEnabled && (
<Menu.Group>
<Menu.Item
testID="postAtUriShareBtn"
label={_(msg`Copy post at:// URI`)}
onPress={onShareATURI}>
<Menu.ItemText>
<Trans>Share post at:// URI</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
<Menu.Item
testID="postAuthorDIDShareBtn"
label={_(msg`Copy author DID`)}
onPress={onShareAuthorDID}>
<Menu.ItemText>
<Trans>Share author DID</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
</Menu.Group>
)}
</Menu.Outer>
<Prompt.Basic
control={pwiWarningShareControl}
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`)}
/>
<Prompt.Basic
control={pwiWarningCopyControl}
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={onCopyLink}
confirmButtonCta={_(msg`Copy anyway`)}
/>
<SendViaChatDialog
control={sendViaChatControl}
onSelectChat={onSelectChatToShareTo}
/>
</>
)
}
ShareMenuItems = memo(ShareMenuItems)
export {ShareMenuItems}
@@ -0,0 +1,22 @@
import {type PressableProps, type StyleProp, type ViewStyle} from 'react-native'
import {
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyFeedThreadgate,
type RichText as RichTextAPI,
} from '@atproto/api'
import {type Shadow} from '#/state/cache/post-shadow'
export interface ShareMenuItemsProps {
testID: string
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
style?: StyleProp<ViewStyle>
hitSlop?: PressableProps['hitSlop']
size?: 'lg' | 'md' | 'sm'
timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record
onShare: () => void
}
@@ -0,0 +1,192 @@
import {memo, useMemo} from 'react'
import {AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import type React from 'react'
import {makeProfileLink} from '#/lib/routes/links'
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 {useProfileShadow} from '#/state/cache/profile-shadow'
import {useSession} from '#/state/session'
import {useBreakpoints} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {EmbedDialog} from '#/components/dialogs/Embed'
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {useDevMode} from '#/storage/hooks/dev-mode'
import {type ShareMenuItemsProps} from './ShareMenuItems.types'
let ShareMenuItems = ({
post,
record,
timestamp,
onShare: onShareProp,
}: ShareMenuItemsProps): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const loggedOutWarningPromptControl = useDialogControl()
const embedPostControl = useDialogControl()
const sendViaChatControl = useDialogControl()
const [devModeEnabled] = useDevMode()
const postUri = post.uri
const postCid = post.cid
const postAuthor = useProfileShadow(post.author)
const href = useMemo(() => {
const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey)
}, [postUri, postAuthor])
const hideInPWI = useMemo(() => {
return !!postAuthor.labels?.find(
label => label.val === '!no-unauthenticated',
)
}, [postAuthor])
const showLoggedOutWarning =
postAuthor.did !== currentAccount?.did && hideInPWI
const onCopyLink = () => {
logger.metric('share:press:copyLink', {}, {statsig: true})
const url = toShareUrl(href)
shareUrl(url)
onShareProp()
}
const onSelectChatToShareTo = (conversation: string) => {
logger.metric('share:press:dmSelected', {}, {statsig: true})
navigation.navigate('MessagesConversation', {
conversation,
embed: postUri,
})
}
const canEmbed = isWeb && gtMobile && !hideInPWI
const onShareATURI = () => {
shareText(postUri)
}
const onShareAuthorDID = () => {
shareText(postAuthor.did)
}
return (
<>
<Menu.Outer>
<Menu.Group>
<Menu.Item
testID="postDropdownShareBtn"
label={_(msg`Copy link to post`)}
onPress={() => {
if (showLoggedOutWarning) {
loggedOutWarningPromptControl.open()
} else {
onCopyLink()
}
}}>
<Menu.ItemText>
<Trans>Copy link to post</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ChainLinkIcon} position="right" />
</Menu.Item>
{hasSession && (
<Menu.Item
testID="postDropdownSendViaDMBtn"
label={_(msg`Send via direct message`)}
onPress={() => {
logger.metric('share:press:openDmSearch', {}, {statsig: true})
sendViaChatControl.open()
}}>
<Menu.ItemText>
<Trans>Send via direct message</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Send} position="right" />
</Menu.Item>
)}
{canEmbed && (
<Menu.Item
testID="postDropdownEmbedBtn"
label={_(msg`Embed post`)}
onPress={() => {
logger.metric('share:press:embed', {}, {statsig: true})
embedPostControl.open()
}}>
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
<Menu.ItemIcon icon={CodeBrackets} 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>
<Trans>Copy post at:// URI</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
<Menu.Item
testID="postAuthorDIDShareBtn"
label={_(msg`Copy author DID`)}
onPress={onShareAuthorDID}>
<Menu.ItemText>
<Trans>Copy author DID</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
</Menu.Group>
</>
)}
</Menu.Outer>
<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={onCopyLink}
confirmButtonCta={_(msg`Share anyway`)}
/>
{canEmbed && (
<EmbedDialog
control={embedPostControl}
postCid={postCid}
postUri={postUri}
record={record}
postAuthor={postAuthor}
timestamp={timestamp}
/>
)}
<SendViaChatDialog
control={sendViaChatControl}
onSelectChat={onSelectChatToShareTo}
/>
</>
)
}
ShareMenuItems = memo(ShareMenuItems)
export {ShareMenuItems}
@@ -0,0 +1,119 @@
import {memo, useMemo, useState} from 'react'
import {
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyFeedThreadgate,
AtUri,
type RichText as RichTextAPI,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import type React from 'react'
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 {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow'
import {EventStopper} from '#/view/com/util/EventStopper'
import {native} from '#/alf'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
import {useMenuControl} from '#/components/Menu'
import * as Menu from '#/components/Menu'
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
import {ShareMenuItems} from './ShareMenuItems'
let ShareMenuButton = ({
testID,
post,
big,
record,
richText,
timestamp,
threadgateRecord,
onShare,
}: {
testID: string
post: Shadow<AppBskyFeedDefs.PostView>
big?: boolean
record: AppBskyFeedPost.Record
richText: RichTextAPI
timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record
onShare: () => void
}): React.ReactNode => {
const {_} = useLingui()
const gate = useGate()
const ShareIcon = gate('alt_share_icon')
? ArrowShareRightIcon
: ArrowOutOfBoxIcon
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)
logger.metric(
'share:open',
{context: big ? 'thread' : 'feed'},
{statsig: true},
)
},
}),
[menuControl, setHasBeenOpen, big],
)
const onNativeLongPress = () => {
logger.metric('share:press:nativeShare', {}, {statsig: true})
const urip = new AtUri(post.uri)
const href = makeProfileLink(post.author, 'post', urip.rkey)
const url = toShareUrl(href)
shareUrl(url)
onShare()
}
return (
<EventStopper onKeyDown={false}>
<Menu.Root control={lazyMenuControl}>
<Menu.Trigger label={_(msg`Open share menu`)}>
{({props}) => {
return (
<PostControlButton
testID="postShareBtn"
big={big}
label={props.accessibilityLabel}
{...props}
onLongPress={native(onNativeLongPress)}>
<PostControlButtonIcon icon={ShareIcon} />
</PostControlButton>
)
}}
</Menu.Trigger>
{hasBeenOpen && (
// Lazily initialized. Once mounted, they stay mounted.
<ShareMenuItems
testID={testID}
post={post}
record={record}
richText={richText}
timestamp={timestamp}
threadgateRecord={threadgateRecord}
onShare={onShare}
/>
)}
</Menu.Root>
</EventStopper>
)
}
ShareMenuButton = memo(ShareMenuButton)
export {ShareMenuButton}
@@ -1,55 +1,43 @@
import React, {memo} from 'react'
import {
Pressable,
type PressableStateCallbackType,
type StyleProp,
View,
type ViewStyle,
} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {memo, useState} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
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 {useRequireAuth} 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 {formatCount} from '#/view/com/util/numeric/format'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints} from '#/alf'
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 {
PostControlButton,
PostControlButtonIcon,
PostControlButtonText,
} from './PostControlButton'
import {PostMenuButton} from './PostMenu'
import {RepostButton} from './RepostButton'
import {ShareMenuButton} from './ShareMenu'
let PostCtrls = ({
let PostControls = ({
big,
post,
record,
@@ -76,25 +64,18 @@ let PostCtrls = ({
threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
}): React.ReactNode => {
const t = useTheme()
const {_, i18n} = useLingui()
const {gtMobile} = useBreakpoints()
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 ||
@@ -102,22 +83,7 @@ let PostCtrls = ({
)
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 [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = useState(false)
const onPressToggleLike = async () => {
if (isBlocked) {
@@ -200,10 +166,6 @@ let PostCtrls = ({
}
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',
@@ -212,20 +174,6 @@ let PostCtrls = ({
})
}
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
@@ -233,39 +181,29 @@ let PostCtrls = ({
big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
replyDisabled ? {opacity: 0.5} : undefined,
]}>
<Pressable
<PostControlButton
testID="replyBtn"
style={btnStyle}
onPress={() => {
if (!replyDisabled) {
playHaptic('Light')
requireAuth(() => onPressReply())
}
}}
accessibilityRole="button"
accessibilityLabel={_(
msg`Reply (${plural(post.replyCount || 0, {
one: '# reply',
other: '# replies',
})})`,
onPress={
!replyDisabled ? () => requireAuth(() => onPressReply()) : undefined
}
label={_(
msg({
message: `Reply (${plural(post.replyCount || 0, {
one: '# reply',
other: '# replies',
})})`,
comment:
'Accessibility label for the reply button, verb form followed by number of replies and noun form',
}),
)}
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,
]}>
big={big}>
<PostControlButtonIcon icon={Bubble} />
{typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
<PostControlButtonText>
{formatCount(i18n, post.replyCount)}
</Text>
) : undefined}
</Pressable>
</PostControlButtonText>
)}
</PostControlButton>
</View>
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
<RepostButton
@@ -278,28 +216,33 @@ let PostCtrls = ({
/>
</View>
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
<Pressable
<PostControlButton
testID="likeBtn"
style={btnStyle}
big={big}
onPress={() => requireAuth(() => onPressToggleLike())}
accessibilityRole="button"
accessibilityLabel={
label={
post.viewer?.like
? _(
msg`Unlike (${plural(post.likeCount || 0, {
one: '# like',
other: '# likes',
})})`,
msg({
message: `Unlike (${plural(post.likeCount || 0, {
one: '# like',
other: '# likes',
})})`,
comment:
'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun',
}),
)
: _(
msg`Like (${plural(post.likeCount || 0, {
one: '# like',
other: '# likes',
})})`,
msg({
message: `Like (${plural(post.likeCount || 0, {
one: '# like',
other: '# likes',
})})`,
comment:
'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form',
}),
)
}
accessibilityHint=""
hitSlop={POST_CTRL_HITSLOP}>
}>
<AnimatedLikeIcon
isLiked={Boolean(post.viewer?.like)}
big={big}
@@ -311,84 +254,37 @@ let PostCtrls = ({
isLiked={Boolean(post.viewer?.like)}
hasBeenToggled={hasLikeIconBeenToggled}
/>
</Pressable>
</PostControlButton>
</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
<ShareMenuButton
testID="postShareBtn"
post={post}
big={big}
record={record}
richText={richText}
timestamp={post.indexedAt}
threadgateRecord={threadgateRecord}
onShare={onShare}
/>
</View>
<View
style={big ? a.align_center : [gtMobile && a.flex_1, a.align_start]}>
<PostMenuButton
testID="postDropdownBtn"
post={post}
postFeedContext={feedContext}
postReqId={reqId}
big={big}
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}
PostControls = memo(PostControls)
export {PostControls}
+5
View File
@@ -3,3 +3,8 @@ import {createSinglePathSVG} from './TEMPLATE'
export const ArrowOutOfBox_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12.707 3.293a1 1 0 0 0-1.414 0l-4.5 4.5a1 1 0 0 0 1.414 1.414L11 6.414v8.836a1 1 0 1 0 2 0V6.414l2.793 2.793a1 1 0 1 0 1.414-1.414l-4.5-4.5ZM5 12.75a1 1 0 1 0-2 0V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-7.25a1 1 0 1 0-2 0V19H5v-6.25Z',
})
export const ArrowOutOfBoxModified_Stroke2_Corner2_Rounded =
createSinglePathSVG({
path: 'M20 13.75a1 1 0 0 1 1 1V18a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3v-3.25a1 1 0 1 1 2 0V18a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.25a1 1 0 0 1 1-1ZM12 3a1 1 0 0 1 .707.293l4.5 4.5a1 1 0 1 1-1.414 1.414L13 6.414v8.836a1 1 0 1 1-2 0V6.414L8.207 9.207a1 1 0 1 1-1.414-1.414l4.5-4.5A1 1 0 0 1 12 3Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ArrowShareRight_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M11.839 4.744c0-1.488 1.724-2.277 2.846-1.364l.107.094 7.66 7.256.128.134c.558.652.558 1.62 0 2.272l-.128.135-7.66 7.255c-1.115 1.057-2.953.267-2.953-1.27v-2.748c-3.503.055-5.417.41-6.592.97-.997.474-1.525 1.122-2.084 2.14l-.243.46c-.558 1.088-2.09.583-2.08-.515l.015-.748c.111-3.68.777-6.5 2.546-8.415 1.83-1.98 4.63-2.771 8.438-2.884V4.744Zm2 3.256c0 .79-.604 1.41-1.341 1.494l-.149.01c-3.9.057-6.147.813-7.48 2.254-.963 1.043-1.562 2.566-1.842 4.79.38-.327.826-.622 1.361-.877 1.656-.788 4.08-1.14 7.938-1.169l.153.007c.754.071 1.36.704 1.36 1.491v2.675L20.884 12l-7.045-6.676V8Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ChainLink_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
})
+1
View File
@@ -1,5 +1,6 @@
export type Gate =
// Keep this alphabetic please.
| 'alt_share_icon'
| 'debug_show_feedcontext'
| 'debug_subscriptions'
| 'explore_show_suggested_feeds'
+8
View File
@@ -395,4 +395,12 @@ export type MetricEvents = {
'live:card:openProfile': {subject: string}
'live:view:profile': {subject: string}
'live:view:post': {subject: string; feed?: string}
'share:open': {context: 'feed' | 'thread'}
'share:press:copyLink': {}
'share:press:nativeShare': {}
'share:press:openDmSearch': {}
'share:press:dmSelected': {}
'share:press:recentDm': {}
'share:press:embed': {}
}
+2 -2
View File
@@ -82,7 +82,6 @@ import {useSetMinimalShellMode} from '#/state/shell'
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
import {List} from '#/view/com/util/List'
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {Header} from '#/screens/VideoFeed/components/Header'
import {atoms as a, ios, platform, ThemeProvider, useTheme} from '#/alf'
@@ -97,6 +96,7 @@ import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {ListFooter} from '#/components/Lists'
import * as Hider from '#/components/moderation/Hider'
import {PostControls} from '#/components/PostControls'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
@@ -861,7 +861,7 @@ function Overlay({
)}
{record && (
<View style={[{left: -5}]}>
<PostCtrls
<PostControls
richText={richText}
post={post}
record={record}
+3 -3
View File
@@ -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}
+11 -11
View File
@@ -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}
+6 -3
View File
@@ -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>
)
+1 -1
View File
@@ -26,7 +26,7 @@ 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 {ArrowOutOfBoxModified_Stroke2_Corner2_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 {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'