Files
bsky-social-app/src/components/PostControls/PostMenu/index.tsx
T
Samuel Newman 2beb63fb2d migrate RichText to the SDK and resolve facets via the appview
The two RichText classes are not mutually assignable - `UnicodeString` has a
private field and the SDK brands `did`/`uri` as template literal types - so
every producer and consumer of a RichText instance has to move in one step.

`detectFacets` now takes a lex client instead of the legacy agent, which is
what removes the last hard agent dependency from these files. Handle
resolution is an appview job, so the appview client is threaded in: through
`useAppviewClient` in the hooks and dialogs, and through a new
`appviewClient` option on `apilib.post` (Composer already had the client to
hand). The rest of the post pipeline still writes through the agent.

Facet feature checks move from the `AppBskyRichtextFacet` validators to the
generated `#/lexicons` schemas, matching how the rest of the app narrows
lexicon types.

Display sinks still read facets off `@atproto/api` view types, which are the
same lexicon but typed with plain strings. `asSdkFacets` widens them at those
call sites and goes away once the view types come from the SDK too.
2026-08-13 21:50:54 +03:00

104 lines
3.0 KiB
TypeScript

import {memo, useMemo, useState} from 'react'
import {type Insets} from 'react-native'
import {
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyFeedThreadgate,
} from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useLingui} from '@lingui/react/macro'
import {type Shadow} from '#/state/cache/post-shadow'
import {EventStopper} from '#/view/com/util/EventStopper'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import * as Menu from '#/components/Menu'
import {useMenuControl} from '#/components/Menu'
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
import {PostMenuItems} from './PostMenuItems'
let PostMenuButton = ({
testID,
post,
postFeedContext,
postReqId,
big,
record,
richText,
timestamp,
threadgateRecord,
onShowLess,
hitSlop,
logContext,
forceGoogleTranslate,
}: {
testID: string
post: Shadow<AppBskyFeedDefs.PostView>
postFeedContext: string | undefined
postReqId: string | undefined
big?: boolean
record: AppBskyFeedPost.Record
richText: RichTextAPI
timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
hitSlop?: Insets
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
forceGoogleTranslate: boolean
}): React.ReactNode => {
const {t: l} = useLingui()
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={l`Open post options menu`}>
{({props}) => {
return (
<PostControlButton
testID="postDropdownBtn"
big={big}
label={props.accessibilityLabel}
{...props}
hitSlop={hitSlop}>
<PostControlButtonIcon icon={DotsHorizontal} />
</PostControlButton>
)
}}
</Menu.Trigger>
{hasBeenOpen && (
// Lazily initialized. Once mounted, they stay mounted.
<PostMenuItems
testID={testID}
post={post}
postFeedContext={postFeedContext}
postReqId={postReqId}
record={record}
richText={richText}
timestamp={timestamp}
threadgateRecord={threadgateRecord}
onShowLess={onShowLess}
logContext={logContext}
forceGoogleTranslate={forceGoogleTranslate}
/>
)}
</Menu.Root>
</EventStopper>
)
}
PostMenuButton = memo(PostMenuButton)
export {PostMenuButton}