forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {
3 type AppBskyActorDefs,
4 type AppBskyFeedDefs,
5 type AppBskyUnspeccedGetPostThreadV2,
6 type BlobRef,
7 type ModerationDecision,
8} from '@atproto/api'
9import {msg} from '@lingui/macro'
10import {useLingui} from '@lingui/react'
11import {useQueryClient} from '@tanstack/react-query'
12
13import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
14import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers'
15import {purgeTemporaryImageFiles} from '#/state/gallery'
16import {precacheResolveLinkQuery} from '#/state/queries/resolve-link'
17import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker'
18import * as Toast from '#/view/com/util/Toast'
19
20export interface ComposerOptsPostRef {
21 uri: string
22 cid: string
23 text: string
24 langs?: string[]
25 author: AppBskyActorDefs.ProfileViewBasic
26 embed?: AppBskyFeedDefs.PostView['embed']
27 moderation?: ModerationDecision
28}
29
30export type OnPostSuccessData =
31 | {
32 replyToUri?: string
33 posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[]
34 }
35 | undefined
36
37export interface ComposerOpts {
38 replyTo?: ComposerOptsPostRef
39 onPost?: (postUri: string | undefined) => void
40 onPostSuccess?: (data: OnPostSuccessData) => void
41 quote?: AppBskyFeedDefs.PostView
42 mention?: string // handle of user to mention
43 openEmojiPicker?: (pos: EmojiPickerPosition | undefined) => void
44 text?: string
45 imageUris?: {
46 uri: string
47 width: number
48 height: number
49 altText?: string
50 blobRef?: BlobRef
51 }[]
52 videoUri?: {
53 uri: string
54 width: number
55 height: number
56 blobRef?: BlobRef
57 altText?: string
58 }
59 openGallery?: boolean
60}
61
62type StateContext = ComposerOpts | undefined
63type ControlsContext = {
64 openComposer: (opts: ComposerOpts) => void
65 closeComposer: () => boolean
66}
67
68const stateContext = React.createContext<StateContext>(undefined)
69stateContext.displayName = 'ComposerStateContext'
70const controlsContext = React.createContext<ControlsContext>({
71 openComposer(_opts: ComposerOpts) {},
72 closeComposer() {
73 return false
74 },
75})
76controlsContext.displayName = 'ComposerControlsContext'
77
78export function Provider({children}: React.PropsWithChildren<{}>) {
79 const {_} = useLingui()
80 const [state, setState] = React.useState<StateContext>()
81 const queryClient = useQueryClient()
82
83 const openComposer = useNonReactiveCallback((opts: ComposerOpts) => {
84 if (opts.quote) {
85 const path = postUriToRelativePath(opts.quote.uri)
86 if (path) {
87 const appUrl = toBskyAppUrl(path)
88 precacheResolveLinkQuery(queryClient, appUrl, {
89 type: 'record',
90 kind: 'post',
91 record: {
92 cid: opts.quote.cid,
93 uri: opts.quote.uri,
94 },
95 view: opts.quote,
96 })
97 }
98 }
99 const author = opts.replyTo?.author || opts.quote?.author
100 const isBlocked = Boolean(
101 author &&
102 (author.viewer?.blocking ||
103 author.viewer?.blockedBy ||
104 author.viewer?.blockingByList),
105 )
106 if (isBlocked) {
107 Toast.show(
108 _(msg`Cannot interact with a blocked user`),
109 'exclamation-circle',
110 )
111 } else {
112 setState(prevOpts => {
113 if (prevOpts) {
114 // Never replace an already open composer.
115 return prevOpts
116 }
117 return opts
118 })
119 }
120 })
121
122 const closeComposer = useNonReactiveCallback(() => {
123 let wasOpen = !!state
124 if (wasOpen) {
125 setState(undefined)
126 purgeTemporaryImageFiles()
127 }
128
129 return wasOpen
130 })
131
132 const api = React.useMemo(
133 () => ({
134 openComposer,
135 closeComposer,
136 }),
137 [openComposer, closeComposer],
138 )
139
140 return (
141 <stateContext.Provider value={state}>
142 <controlsContext.Provider value={api}>
143 {children}
144 </controlsContext.Provider>
145 </stateContext.Provider>
146 )
147}
148
149export function useComposerState() {
150 return React.useContext(stateContext)
151}
152
153export function useComposerControls() {
154 const {closeComposer} = React.useContext(controlsContext)
155 return React.useMemo(() => ({closeComposer}), [closeComposer])
156}
157
158/**
159 * DO NOT USE DIRECTLY. The deprecation notice as a warning only, it's not
160 * actually deprecated.
161 *
162 * @deprecated use `#/lib/hooks/useOpenComposer` instead
163 */
164export function useOpenComposer() {
165 const {openComposer} = React.useContext(controlsContext)
166 return React.useMemo(() => ({openComposer}), [openComposer])
167}