Files
bsky-social-app/src/state/shell/composer/index.tsx
T
Samuel Newman 22bac72a88 [Drafts] Metrics (#9794)
* metrics for drafts

* Nit: format

* nit: use new util for clarity

* nit: use new util for clarity

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-29 15:08:09 -06:00

164 lines
4.4 KiB
TypeScript

import React from 'react'
import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
type AppBskyUnspeccedGetPostThreadV2,
type ModerationDecision,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers'
import {purgeTemporaryImageFiles} from '#/state/gallery'
import {precacheResolveLinkQuery} from '#/state/queries/resolve-link'
import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker'
import * as Toast from '#/view/com/util/Toast'
export interface ComposerOptsPostRef {
uri: string
cid: string
text: string
langs?: string[]
author: AppBskyActorDefs.ProfileViewBasic
embed?: AppBskyFeedDefs.PostView['embed']
moderation?: ModerationDecision
}
export type OnPostSuccessData =
| {
replyToUri?: string
posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[]
}
| undefined
export type ComposerLogContext =
| 'Fab'
| 'PostReply'
| 'QuotePost'
| 'ProfileFeed'
| 'Deeplink'
| 'Other'
export interface ComposerOpts {
replyTo?: ComposerOptsPostRef
onPost?: (postUri: string | undefined) => void
onPostSuccess?: (data: OnPostSuccessData) => void
quote?: AppBskyFeedDefs.PostView
mention?: string // handle of user to mention
openEmojiPicker?: (pos: EmojiPickerPosition | undefined) => void
text?: string
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
videoUri?: {uri: string; width: number; height: number}
openGallery?: boolean
logContext?: ComposerLogContext
}
type StateContext = ComposerOpts | undefined
type ControlsContext = {
openComposer: (opts: ComposerOpts) => void
closeComposer: () => boolean
}
const stateContext = React.createContext<StateContext>(undefined)
stateContext.displayName = 'ComposerStateContext'
const controlsContext = React.createContext<ControlsContext>({
openComposer(_opts: ComposerOpts) {},
closeComposer() {
return false
},
})
controlsContext.displayName = 'ComposerControlsContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const {_} = useLingui()
const [state, setState] = React.useState<StateContext>()
const queryClient = useQueryClient()
const openComposer = useNonReactiveCallback((opts: ComposerOpts) => {
if (opts.quote) {
const path = postUriToRelativePath(opts.quote.uri)
if (path) {
const appUrl = toBskyAppUrl(path)
precacheResolveLinkQuery(queryClient, appUrl, {
type: 'record',
kind: 'post',
record: {
cid: opts.quote.cid,
uri: opts.quote.uri,
},
view: opts.quote,
})
}
}
const author = opts.replyTo?.author || opts.quote?.author
const isBlocked = Boolean(
author &&
(author.viewer?.blocking ||
author.viewer?.blockedBy ||
author.viewer?.blockingByList),
)
if (isBlocked) {
Toast.show(
_(msg`Cannot interact with a blocked user`),
'exclamation-circle',
)
} else {
setState(prevOpts => {
if (prevOpts) {
// Never replace an already open composer.
return prevOpts
}
return opts
})
}
})
const closeComposer = useNonReactiveCallback(() => {
let wasOpen = !!state
if (wasOpen) {
setState(undefined)
purgeTemporaryImageFiles()
}
return wasOpen
})
const api = React.useMemo(
() => ({
openComposer,
closeComposer,
}),
[openComposer, closeComposer],
)
return (
<stateContext.Provider value={state}>
<controlsContext.Provider value={api}>
{children}
</controlsContext.Provider>
</stateContext.Provider>
)
}
export function useComposerState() {
return React.useContext(stateContext)
}
export function useComposerControls() {
const {closeComposer} = React.useContext(controlsContext)
return React.useMemo(() => ({closeComposer}), [closeComposer])
}
/**
* DO NOT USE DIRECTLY. The deprecation notice as a warning only, it's not
* actually deprecated.
*
* @deprecated use `#/lib/hooks/useOpenComposer` instead
*/
export function useOpenComposer() {
const {openComposer} = React.useContext(controlsContext)
return React.useMemo(() => ({openComposer}), [openComposer])
}