mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at remove-hackfix 325 lines 9.0 kB view raw
1import {useCallback, useMemo, useState} from 'react' 2import {useQuery, useQueryClient} from '@tanstack/react-query' 3 4import {isWeb} from '#/platform/detection' 5import {useModerationOpts} from '#/state/preferences/moderation-opts' 6import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences' 7import { 8 LINEAR_VIEW_BELOW, 9 LINEAR_VIEW_BF, 10 TREE_VIEW_BELOW, 11 TREE_VIEW_BELOW_DESKTOP, 12 TREE_VIEW_BF, 13} from '#/state/queries/usePostThread/const' 14import { 15 createCacheMutator, 16 getThreadPlaceholder, 17} from '#/state/queries/usePostThread/queryCache' 18import { 19 buildThread, 20 sortAndAnnotateThreadItems, 21} from '#/state/queries/usePostThread/traversal' 22import { 23 createPostThreadOtherQueryKey, 24 createPostThreadQueryKey, 25 type ThreadItem, 26 type UsePostThreadQueryResult, 27} from '#/state/queries/usePostThread/types' 28import {getThreadgateRecord} from '#/state/queries/usePostThread/utils' 29import * as views from '#/state/queries/usePostThread/views' 30import {useAgent, useSession} from '#/state/session' 31import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 32import {useBreakpoints} from '#/alf' 33 34export * from '#/state/queries/usePostThread/types' 35 36export function usePostThread({anchor}: {anchor?: string}) { 37 const qc = useQueryClient() 38 const agent = useAgent() 39 const {hasSession} = useSession() 40 const {gtPhone} = useBreakpoints() 41 const moderationOpts = useModerationOpts() 42 const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies() 43 const { 44 isLoaded: isThreadPreferencesLoaded, 45 sort, 46 setSort: baseSetSort, 47 view, 48 setView: baseSetView, 49 prioritizeFollowedUsers, 50 } = useThreadPreferences() 51 const below = useMemo(() => { 52 return view === 'linear' 53 ? LINEAR_VIEW_BELOW 54 : isWeb && gtPhone 55 ? TREE_VIEW_BELOW_DESKTOP 56 : TREE_VIEW_BELOW 57 }, [view, gtPhone]) 58 59 const postThreadQueryKey = createPostThreadQueryKey({ 60 anchor, 61 sort, 62 view, 63 prioritizeFollowedUsers, 64 }) 65 const postThreadOtherQueryKey = createPostThreadOtherQueryKey({ 66 anchor, 67 prioritizeFollowedUsers, 68 }) 69 70 const query = useQuery<UsePostThreadQueryResult>({ 71 enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts, 72 queryKey: postThreadQueryKey, 73 async queryFn(ctx) { 74 const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ 75 anchor: anchor!, 76 branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, 77 below, 78 sort: sort, 79 prioritizeFollowedUsers: prioritizeFollowedUsers, 80 }) 81 82 /* 83 * Initialize `ctx.meta` to track if we know we have additional replies 84 * we could fetch once we hit the end. 85 */ 86 ctx.meta = ctx.meta || { 87 hasOtherReplies: false, 88 } 89 90 /* 91 * If we know we have additional replies, we'll set this to true. 92 */ 93 if (data.hasOtherReplies) { 94 ctx.meta.hasOtherReplies = true 95 } 96 97 const result = { 98 thread: data.thread || [], 99 threadgate: data.threadgate, 100 hasOtherReplies: !!ctx.meta.hasOtherReplies, 101 } 102 103 const record = getThreadgateRecord(result.threadgate) 104 if (result.threadgate && record) { 105 result.threadgate.record = record 106 } 107 108 return result as UsePostThreadQueryResult 109 }, 110 placeholderData() { 111 if (!anchor) return 112 const placeholder = getThreadPlaceholder(qc, anchor) 113 /* 114 * Always return something here, even empty data, so that 115 * `isPlaceholderData` is always true, which we'll use to insert 116 * skeletons. 117 */ 118 const thread = placeholder ? [placeholder] : [] 119 return {thread, threadgate: undefined, hasOtherReplies: false} 120 }, 121 select(data) { 122 const record = getThreadgateRecord(data.threadgate) 123 if (data.threadgate && record) { 124 data.threadgate.record = record 125 } 126 return data 127 }, 128 }) 129 130 const thread = useMemo(() => query.data?.thread || [], [query.data?.thread]) 131 const threadgate = useMemo( 132 () => query.data?.threadgate, 133 [query.data?.threadgate], 134 ) 135 const hasOtherThreadItems = useMemo( 136 () => !!query.data?.hasOtherReplies, 137 [query.data?.hasOtherReplies], 138 ) 139 const [otherItemsVisible, setOtherItemsVisible] = useState(false) 140 141 /** 142 * Creates a mutator for the post thread cache. This is used to insert 143 * replies into the thread cache after posting. 144 */ 145 const mutator = useMemo( 146 () => 147 createCacheMutator({ 148 params: {view, below}, 149 postThreadQueryKey, 150 postThreadOtherQueryKey, 151 queryClient: qc, 152 }), 153 [qc, view, below, postThreadQueryKey, postThreadOtherQueryKey], 154 ) 155 156 /** 157 * If we have additional items available from the server and the user has 158 * chosen to view them, start loading data 159 */ 160 const additionalQueryEnabled = hasOtherThreadItems && otherItemsVisible 161 const additionalItemsQuery = useQuery({ 162 enabled: additionalQueryEnabled, 163 queryKey: postThreadOtherQueryKey, 164 async queryFn() { 165 const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({ 166 anchor: anchor!, 167 prioritizeFollowedUsers, 168 }) 169 return data 170 }, 171 }) 172 const serverOtherThreadItems: ThreadItem[] = useMemo(() => { 173 if (!additionalQueryEnabled) return [] 174 if (additionalItemsQuery.isLoading) { 175 return Array.from({length: 2}).map((_, i) => 176 views.skeleton({ 177 key: `other-reply-${i}`, 178 item: 'reply', 179 }), 180 ) 181 } else if (additionalItemsQuery.isError) { 182 /* 183 * We could insert an special error component in here, but since these 184 * are optional additional replies, it's not critical that they're shown 185 * atm. 186 */ 187 return [] 188 } else if (additionalItemsQuery.data?.thread) { 189 const {threadItems} = sortAndAnnotateThreadItems( 190 additionalItemsQuery.data.thread, 191 { 192 view, 193 skipModerationHandling: true, 194 threadgateHiddenReplies: mergeThreadgateHiddenReplies( 195 threadgate?.record, 196 ), 197 moderationOpts: moderationOpts!, 198 }, 199 ) 200 return threadItems 201 } else { 202 return [] 203 } 204 }, [ 205 view, 206 additionalQueryEnabled, 207 additionalItemsQuery, 208 mergeThreadgateHiddenReplies, 209 moderationOpts, 210 threadgate?.record, 211 ]) 212 213 /** 214 * Sets the sort order for the thread and resets the additional thread items 215 */ 216 const setSort: typeof baseSetSort = useCallback( 217 nextSort => { 218 setOtherItemsVisible(false) 219 baseSetSort(nextSort) 220 }, 221 [baseSetSort, setOtherItemsVisible], 222 ) 223 224 /** 225 * Sets the view variant for the thread and resets the additional thread items 226 */ 227 const setView: typeof baseSetView = useCallback( 228 nextView => { 229 setOtherItemsVisible(false) 230 baseSetView(nextView) 231 }, 232 [baseSetView, setOtherItemsVisible], 233 ) 234 235 /* 236 * This is the main thread response, sorted into separate buckets based on 237 * moderation, and annotated with all UI state needed for rendering. 238 */ 239 const {threadItems, otherThreadItems} = useMemo(() => { 240 return sortAndAnnotateThreadItems(thread, { 241 view: view, 242 threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record), 243 moderationOpts: moderationOpts!, 244 }) 245 }, [ 246 thread, 247 threadgate?.record, 248 mergeThreadgateHiddenReplies, 249 moderationOpts, 250 view, 251 ]) 252 253 /* 254 * Take all three sets of thread items and combine them into a single thread, 255 * along with any other thread items required for rendering e.g. "Show more 256 * replies" or the reply composer. 257 */ 258 const items = useMemo(() => { 259 return buildThread({ 260 threadItems, 261 otherThreadItems, 262 serverOtherThreadItems, 263 isLoading: query.isPlaceholderData, 264 hasSession, 265 hasOtherThreadItems, 266 otherItemsVisible, 267 showOtherItems: () => setOtherItemsVisible(true), 268 }) 269 }, [ 270 threadItems, 271 otherThreadItems, 272 serverOtherThreadItems, 273 query.isPlaceholderData, 274 hasSession, 275 hasOtherThreadItems, 276 otherItemsVisible, 277 setOtherItemsVisible, 278 ]) 279 280 return useMemo( 281 () => ({ 282 state: { 283 /* 284 * Copy in any query state that is useful 285 */ 286 isFetching: query.isFetching, 287 isPlaceholderData: query.isPlaceholderData, 288 error: query.error, 289 /* 290 * Other state 291 */ 292 sort, 293 view, 294 otherItemsVisible, 295 }, 296 data: { 297 items, 298 threadgate, 299 }, 300 actions: { 301 /* 302 * Copy in any query actions that are useful 303 */ 304 insertReplies: mutator.insertReplies, 305 refetch: query.refetch, 306 /* 307 * Other actions 308 */ 309 setSort, 310 setView, 311 }, 312 }), 313 [ 314 query, 315 mutator.insertReplies, 316 otherItemsVisible, 317 sort, 318 view, 319 setSort, 320 setView, 321 threadgate, 322 items, 323 ], 324 ) 325}