···1212import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
1313import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
1414import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
1515-import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread'
1615import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
1716import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
1817import {castAsShadow, type Shadow} from './types'
···175174 }
176175 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
177176 yield post
178178- }
179179- for (let node of findAllPostsInThreadQueryData(queryClient, uri)) {
180180- if (node.type === 'post') {
181181- yield node.post
182182- }
183177 }
184178 for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) {
185179 yield post
-2
src/state/cache/profile-shadow.ts
···1616import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
1717import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
1818import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
1919-import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '#/state/queries/post-thread'
2019import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile'
2120import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
2221import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
···175174 yield* findAllProfilesInActorSearchQueryData(queryClient, did)
176175 yield* findAllProfilesInListConvosQueryData(queryClient, did)
177176 yield* findAllProfilesInFeedsQueryData(queryClient, did)
178178- yield* findAllProfilesInPostThreadQueryData(queryClient, did)
179177 yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
180178 yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
181179 yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
-631
src/state/queries/post-thread.ts
···11-import {
22- type AppBskyActorDefs,
33- type AppBskyEmbedRecord,
44- AppBskyFeedDefs,
55- type AppBskyFeedGetPostThread,
66- AppBskyFeedPost,
77- AtUri,
88- moderatePost,
99- type ModerationDecision,
1010- type ModerationOpts,
1111-} from '@atproto/api'
1212-import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
1313-1414-import {
1515- findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData,
1616- findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData,
1717-} from '#/state/queries/explore-feed-previews'
1818-import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
1919-import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
2020-import {
2121- findAllPostsInQueryData as findAllPostsInSearchQueryData,
2222- findAllProfilesInQueryData as findAllProfilesInSearchQueryData,
2323-} from '#/state/queries/search-posts'
2424-import {useAgent} from '#/state/session'
2525-import * as bsky from '#/types/bsky'
2626-import {
2727- findAllPostsInQueryData as findAllPostsInNotifsQueryData,
2828- findAllProfilesInQueryData as findAllProfilesInNotifsQueryData,
2929-} from './notifications/feed'
3030-import {
3131- findAllPostsInQueryData as findAllPostsInFeedQueryData,
3232- findAllProfilesInQueryData as findAllProfilesInFeedQueryData,
3333-} from './post-feed'
3434-import {
3535- didOrHandleUriMatches,
3636- embedViewRecordToPostView,
3737- getEmbeddedPost,
3838-} from './util'
3939-4040-const REPLY_TREE_DEPTH = 10
4141-export const RQKEY_ROOT = 'post-thread'
4242-export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
4343-type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
4444-4545-export interface ThreadCtx {
4646- depth: number
4747- isHighlightedPost?: boolean
4848- hasMore?: boolean
4949- isParentLoading?: boolean
5050- isChildLoading?: boolean
5151- isSelfThread?: boolean
5252- hasMoreSelfThread?: boolean
5353-}
5454-5555-export type ThreadPost = {
5656- type: 'post'
5757- _reactKey: string
5858- uri: string
5959- post: AppBskyFeedDefs.PostView
6060- record: AppBskyFeedPost.Record
6161- parent: ThreadNode | undefined
6262- replies: ThreadNode[] | undefined
6363- hasOPLike: boolean | undefined
6464- ctx: ThreadCtx
6565-}
6666-6767-export type ThreadNotFound = {
6868- type: 'not-found'
6969- _reactKey: string
7070- uri: string
7171- ctx: ThreadCtx
7272-}
7373-7474-export type ThreadBlocked = {
7575- type: 'blocked'
7676- _reactKey: string
7777- uri: string
7878- ctx: ThreadCtx
7979-}
8080-8181-export type ThreadUnknown = {
8282- type: 'unknown'
8383- uri: string
8484-}
8585-8686-export type ThreadNode =
8787- | ThreadPost
8888- | ThreadNotFound
8989- | ThreadBlocked
9090- | ThreadUnknown
9191-9292-export type ThreadModerationCache = WeakMap<ThreadNode, ModerationDecision>
9393-9494-export type PostThreadQueryData = {
9595- thread: ThreadNode
9696- threadgate?: AppBskyFeedDefs.ThreadgateView
9797-}
9898-9999-export function usePostThreadQuery(uri: string | undefined) {
100100- const queryClient = useQueryClient()
101101- const agent = useAgent()
102102- return useQuery<PostThreadQueryData, Error>({
103103- gcTime: 0,
104104- queryKey: RQKEY(uri || ''),
105105- async queryFn() {
106106- const res = await agent.getPostThread({
107107- uri: uri!,
108108- depth: REPLY_TREE_DEPTH,
109109- })
110110- if (res.success) {
111111- const thread = responseToThreadNodes(res.data.thread)
112112- annotateSelfThread(thread)
113113- return {
114114- thread,
115115- threadgate: res.data.threadgate as
116116- | AppBskyFeedDefs.ThreadgateView
117117- | undefined,
118118- }
119119- }
120120- return {thread: {type: 'unknown', uri: uri!}}
121121- },
122122- enabled: !!uri,
123123- placeholderData: () => {
124124- if (!uri) return
125125- const post = findPostInQueryData(queryClient, uri)
126126- if (post) {
127127- return {thread: post}
128128- }
129129- return undefined
130130- },
131131- })
132132-}
133133-134134-export function fillThreadModerationCache(
135135- cache: ThreadModerationCache,
136136- node: ThreadNode,
137137- moderationOpts: ModerationOpts,
138138-) {
139139- if (node.type === 'post') {
140140- cache.set(node, moderatePost(node.post, moderationOpts))
141141- if (node.parent) {
142142- fillThreadModerationCache(cache, node.parent, moderationOpts)
143143- }
144144- if (node.replies) {
145145- for (const reply of node.replies) {
146146- fillThreadModerationCache(cache, reply, moderationOpts)
147147- }
148148- }
149149- }
150150-}
151151-152152-export function sortThread(
153153- node: ThreadNode,
154154- opts: UsePreferencesQueryResponse['threadViewPrefs'],
155155- modCache: ThreadModerationCache,
156156- currentDid: string | undefined,
157157- justPostedUris: Set<string>,
158158- threadgateRecordHiddenReplies: Set<string>,
159159- fetchedAtCache: Map<string, number>,
160160- fetchedAt: number,
161161- randomCache: Map<string, number>,
162162-): ThreadNode {
163163- if (node.type !== 'post') {
164164- return node
165165- }
166166- if (node.replies) {
167167- node.replies.sort((a: ThreadNode, b: ThreadNode) => {
168168- if (a.type !== 'post') {
169169- return 1
170170- }
171171- if (b.type !== 'post') {
172172- return -1
173173- }
174174-175175- if (node.ctx.isHighlightedPost || opts.lab_treeViewEnabled) {
176176- const aIsJustPosted =
177177- a.post.author.did === currentDid && justPostedUris.has(a.post.uri)
178178- const bIsJustPosted =
179179- b.post.author.did === currentDid && justPostedUris.has(b.post.uri)
180180- if (aIsJustPosted && bIsJustPosted) {
181181- return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
182182- } else if (aIsJustPosted) {
183183- return -1 // reply while onscreen
184184- } else if (bIsJustPosted) {
185185- return 1 // reply while onscreen
186186- }
187187- }
188188-189189- const aIsByOp = a.post.author.did === node.post?.author.did
190190- const bIsByOp = b.post.author.did === node.post?.author.did
191191- if (aIsByOp && bIsByOp) {
192192- return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
193193- } else if (aIsByOp) {
194194- return -1 // op's own reply
195195- } else if (bIsByOp) {
196196- return 1 // op's own reply
197197- }
198198-199199- const aIsBySelf = a.post.author.did === currentDid
200200- const bIsBySelf = b.post.author.did === currentDid
201201- if (aIsBySelf && bIsBySelf) {
202202- return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
203203- } else if (aIsBySelf) {
204204- return -1 // current account's reply
205205- } else if (bIsBySelf) {
206206- return 1 // current account's reply
207207- }
208208-209209- const aHidden = threadgateRecordHiddenReplies.has(a.uri)
210210- const bHidden = threadgateRecordHiddenReplies.has(b.uri)
211211- if (aHidden && !aIsBySelf && !bHidden) {
212212- return 1
213213- } else if (bHidden && !bIsBySelf && !aHidden) {
214214- return -1
215215- }
216216-217217- const aBlur = Boolean(modCache.get(a)?.ui('contentList').blur)
218218- const bBlur = Boolean(modCache.get(b)?.ui('contentList').blur)
219219- if (aBlur !== bBlur) {
220220- if (aBlur) {
221221- return 1
222222- }
223223- if (bBlur) {
224224- return -1
225225- }
226226- }
227227-228228- const aPin = Boolean(a.record.text.trim() === '📌')
229229- const bPin = Boolean(b.record.text.trim() === '📌')
230230- if (aPin !== bPin) {
231231- if (aPin) {
232232- return 1
233233- }
234234- if (bPin) {
235235- return -1
236236- }
237237- }
238238-239239- if (opts.prioritizeFollowedUsers) {
240240- const af = a.post.author.viewer?.following
241241- const bf = b.post.author.viewer?.following
242242- if (af && !bf) {
243243- return -1
244244- } else if (!af && bf) {
245245- return 1
246246- }
247247- }
248248-249249- // Split items from different fetches into separate generations.
250250- let aFetchedAt = fetchedAtCache.get(a.uri)
251251- if (aFetchedAt === undefined) {
252252- fetchedAtCache.set(a.uri, fetchedAt)
253253- aFetchedAt = fetchedAt
254254- }
255255- let bFetchedAt = fetchedAtCache.get(b.uri)
256256- if (bFetchedAt === undefined) {
257257- fetchedAtCache.set(b.uri, fetchedAt)
258258- bFetchedAt = fetchedAt
259259- }
260260-261261- if (aFetchedAt !== bFetchedAt) {
262262- return aFetchedAt - bFetchedAt // older fetches first
263263- } else if (opts.sort === 'hotness') {
264264- const aHotness = getHotness(a, aFetchedAt)
265265- const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */)
266266- return bHotness - aHotness
267267- } else if (opts.sort === 'oldest') {
268268- return a.post.indexedAt.localeCompare(b.post.indexedAt)
269269- } else if (opts.sort === 'newest') {
270270- return b.post.indexedAt.localeCompare(a.post.indexedAt)
271271- } else if (opts.sort === 'most-likes') {
272272- if (a.post.likeCount === b.post.likeCount) {
273273- return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
274274- } else {
275275- return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
276276- }
277277- } else if (opts.sort === 'random') {
278278- let aRandomScore = randomCache.get(a.uri)
279279- if (aRandomScore === undefined) {
280280- aRandomScore = Math.random()
281281- randomCache.set(a.uri, aRandomScore)
282282- }
283283- let bRandomScore = randomCache.get(b.uri)
284284- if (bRandomScore === undefined) {
285285- bRandomScore = Math.random()
286286- randomCache.set(b.uri, bRandomScore)
287287- }
288288- // this is vaguely criminal but we can get away with it
289289- return aRandomScore - bRandomScore
290290- } else {
291291- return b.post.indexedAt.localeCompare(a.post.indexedAt)
292292- }
293293- })
294294- node.replies.forEach(reply =>
295295- sortThread(
296296- reply,
297297- opts,
298298- modCache,
299299- currentDid,
300300- justPostedUris,
301301- threadgateRecordHiddenReplies,
302302- fetchedAtCache,
303303- fetchedAt,
304304- randomCache,
305305- ),
306306- )
307307- }
308308- return node
309309-}
310310-311311-// internal methods
312312-// =
313313-314314-// Inspired by https://join-lemmy.org/docs/contributors/07-ranking-algo.html
315315-// We want to give recent comments a real chance (and not bury them deep below the fold)
316316-// while also surfacing well-liked comments from the past. In the future, we can explore
317317-// something more sophisticated, but we don't have much data on the client right now.
318318-function getHotness(threadPost: ThreadPost, fetchedAt: number) {
319319- const {post, hasOPLike} = threadPost
320320- const hoursAgo = Math.max(
321321- 0,
322322- (new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
323323- (1000 * 60 * 60),
324324- )
325325- const likeCount = post.likeCount ?? 0
326326- const likeOrder = Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)
327327- const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
328328- const opLikeBoost = hasOPLike ? 0.8 : 1.0
329329- const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost)
330330- return likeOrder / timePenalty
331331-}
332332-333333-function responseToThreadNodes(
334334- node: ThreadViewNode,
335335- depth = 0,
336336- direction: 'up' | 'down' | 'start' = 'start',
337337-): ThreadNode {
338338- if (
339339- AppBskyFeedDefs.isThreadViewPost(node) &&
340340- bsky.dangerousIsType<AppBskyFeedPost.Record>(
341341- node.post.record,
342342- AppBskyFeedPost.isRecord,
343343- )
344344- ) {
345345- const post = node.post
346346- // These should normally be present. They're missing only for
347347- // posts that were *just* created. Ideally, the backend would
348348- // know to return zeros. Fill them in manually to compensate.
349349- post.replyCount ??= 0
350350- post.likeCount ??= 0
351351- post.repostCount ??= 0
352352- return {
353353- type: 'post',
354354- _reactKey: node.post.uri,
355355- uri: node.post.uri,
356356- post: post,
357357- record: node.post.record,
358358- parent:
359359- node.parent && direction !== 'down'
360360- ? responseToThreadNodes(node.parent, depth - 1, 'up')
361361- : undefined,
362362- replies:
363363- node.replies?.length && direction !== 'up'
364364- ? node.replies
365365- .map(reply => responseToThreadNodes(reply, depth + 1, 'down'))
366366- // do not show blocked posts in replies
367367- .filter(node => node.type !== 'blocked')
368368- : undefined,
369369- hasOPLike: Boolean(node?.threadContext?.rootAuthorLike),
370370- ctx: {
371371- depth,
372372- isHighlightedPost: depth === 0,
373373- hasMore:
374374- direction === 'down' && !node.replies?.length && !!post.replyCount,
375375- isSelfThread: false, // populated `annotateSelfThread`
376376- hasMoreSelfThread: false, // populated in `annotateSelfThread`
377377- },
378378- }
379379- } else if (AppBskyFeedDefs.isBlockedPost(node)) {
380380- return {type: 'blocked', _reactKey: node.uri, uri: node.uri, ctx: {depth}}
381381- } else if (AppBskyFeedDefs.isNotFoundPost(node)) {
382382- return {type: 'not-found', _reactKey: node.uri, uri: node.uri, ctx: {depth}}
383383- } else {
384384- return {type: 'unknown', uri: ''}
385385- }
386386-}
387387-388388-function annotateSelfThread(thread: ThreadNode) {
389389- if (thread.type !== 'post') {
390390- return
391391- }
392392- const selfThreadNodes: ThreadPost[] = [thread]
393393-394394- let parent: ThreadNode | undefined = thread.parent
395395- while (parent) {
396396- if (
397397- parent.type !== 'post' ||
398398- parent.post.author.did !== thread.post.author.did
399399- ) {
400400- // not a self-thread
401401- return
402402- }
403403- selfThreadNodes.unshift(parent)
404404- parent = parent.parent
405405- }
406406-407407- let node = thread
408408- for (let i = 0; i < 10; i++) {
409409- const reply = node.replies?.find(
410410- r => r.type === 'post' && r.post.author.did === thread.post.author.did,
411411- )
412412- if (reply?.type !== 'post') {
413413- break
414414- }
415415- selfThreadNodes.push(reply)
416416- node = reply
417417- }
418418-419419- if (selfThreadNodes.length > 1) {
420420- for (const selfThreadNode of selfThreadNodes) {
421421- selfThreadNode.ctx.isSelfThread = true
422422- }
423423- const last = selfThreadNodes[selfThreadNodes.length - 1]
424424- if (
425425- last &&
426426- last.ctx.depth === REPLY_TREE_DEPTH && // at the edge of the tree depth
427427- last.post.replyCount && // has replies
428428- !last.replies?.length // replies were not hydrated
429429- ) {
430430- last.ctx.hasMoreSelfThread = true
431431- }
432432- }
433433-}
434434-435435-function findPostInQueryData(
436436- queryClient: QueryClient,
437437- uri: string,
438438-): ThreadNode | void {
439439- let partial
440440- for (let item of findAllPostsInQueryData(queryClient, uri)) {
441441- if (item.type === 'post') {
442442- // Currently, the backend doesn't send full post info in some cases
443443- // (for example, for quoted posts). We use missing `likeCount`
444444- // as a way to detect that. In the future, we should fix this on
445445- // the backend, which will let us always stop on the first result.
446446- const hasAllInfo = item.post.likeCount != null
447447- if (hasAllInfo) {
448448- return item
449449- } else {
450450- partial = item
451451- // Keep searching, we might still find a full post in the cache.
452452- }
453453- }
454454- }
455455- return partial
456456-}
457457-458458-export function* findAllPostsInQueryData(
459459- queryClient: QueryClient,
460460- uri: string,
461461-): Generator<ThreadNode, void> {
462462- const atUri = new AtUri(uri)
463463-464464- const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({
465465- queryKey: [RQKEY_ROOT],
466466- })
467467- for (const [_queryKey, queryData] of queryDatas) {
468468- if (!queryData) {
469469- continue
470470- }
471471- const {thread} = queryData
472472- for (const item of traverseThread(thread)) {
473473- if (item.type === 'post' && didOrHandleUriMatches(atUri, item.post)) {
474474- const placeholder = threadNodeToPlaceholderThread(item)
475475- if (placeholder) {
476476- yield placeholder
477477- }
478478- }
479479- const quotedPost =
480480- item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
481481- if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
482482- yield embedViewRecordToPlaceholderThread(quotedPost)
483483- }
484484- }
485485- }
486486- for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
487487- // Check notifications first. If you have a post in notifications,
488488- // it's often due to a like or a repost, and we want to prioritize
489489- // a post object with >0 likes/reposts over a stale version with no
490490- // metrics in order to avoid a notification->post scroll jump.
491491- yield postViewToPlaceholderThread(post)
492492- }
493493- for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
494494- yield postViewToPlaceholderThread(post)
495495- }
496496- for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) {
497497- yield postViewToPlaceholderThread(post)
498498- }
499499- for (let post of findAllPostsInSearchQueryData(queryClient, uri)) {
500500- yield postViewToPlaceholderThread(post)
501501- }
502502- for (let post of findAllPostsInExploreFeedPreviewsQueryData(
503503- queryClient,
504504- uri,
505505- )) {
506506- yield postViewToPlaceholderThread(post)
507507- }
508508-}
509509-510510-export function* findAllProfilesInQueryData(
511511- queryClient: QueryClient,
512512- did: string,
513513-): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
514514- const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({
515515- queryKey: [RQKEY_ROOT],
516516- })
517517- for (const [_queryKey, queryData] of queryDatas) {
518518- if (!queryData) {
519519- continue
520520- }
521521- const {thread} = queryData
522522- for (const item of traverseThread(thread)) {
523523- if (item.type === 'post' && item.post.author.did === did) {
524524- yield item.post.author
525525- }
526526- const quotedPost =
527527- item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
528528- if (quotedPost?.author.did === did) {
529529- yield quotedPost?.author
530530- }
531531- }
532532- }
533533- for (let profile of findAllProfilesInFeedQueryData(queryClient, did)) {
534534- yield profile
535535- }
536536- for (let profile of findAllProfilesInNotifsQueryData(queryClient, did)) {
537537- yield profile
538538- }
539539- for (let profile of findAllProfilesInSearchQueryData(queryClient, did)) {
540540- yield profile
541541- }
542542- for (let profile of findAllProfilesInExploreFeedPreviewsQueryData(
543543- queryClient,
544544- did,
545545- )) {
546546- yield profile
547547- }
548548-}
549549-550550-function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {
551551- if (node.type === 'post') {
552552- if (node.parent) {
553553- yield* traverseThread(node.parent)
554554- }
555555- yield node
556556- if (node.replies?.length) {
557557- for (const reply of node.replies) {
558558- yield* traverseThread(reply)
559559- }
560560- }
561561- }
562562-}
563563-564564-function threadNodeToPlaceholderThread(
565565- node: ThreadNode,
566566-): ThreadNode | undefined {
567567- if (node.type !== 'post') {
568568- return undefined
569569- }
570570- return {
571571- type: node.type,
572572- _reactKey: node._reactKey,
573573- uri: node.uri,
574574- post: node.post,
575575- record: node.record,
576576- parent: undefined,
577577- replies: undefined,
578578- hasOPLike: undefined,
579579- ctx: {
580580- depth: 0,
581581- isHighlightedPost: true,
582582- hasMore: false,
583583- isParentLoading: !!node.record.reply,
584584- isChildLoading: !!node.post.replyCount,
585585- },
586586- }
587587-}
588588-589589-function postViewToPlaceholderThread(
590590- post: AppBskyFeedDefs.PostView,
591591-): ThreadNode {
592592- return {
593593- type: 'post',
594594- _reactKey: post.uri,
595595- uri: post.uri,
596596- post: post,
597597- record: post.record as AppBskyFeedPost.Record, // validated in notifs
598598- parent: undefined,
599599- replies: undefined,
600600- hasOPLike: undefined,
601601- ctx: {
602602- depth: 0,
603603- isHighlightedPost: true,
604604- hasMore: false,
605605- isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply,
606606- isChildLoading: true, // assume yes (show the spinner) just in case
607607- },
608608- }
609609-}
610610-611611-function embedViewRecordToPlaceholderThread(
612612- record: AppBskyEmbedRecord.ViewRecord,
613613-): ThreadNode {
614614- return {
615615- type: 'post',
616616- _reactKey: record.uri,
617617- uri: record.uri,
618618- post: embedViewRecordToPostView(record),
619619- record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost
620620- parent: undefined,
621621- replies: undefined,
622622- hasOPLike: undefined,
623623- ctx: {
624624- depth: 0,
625625- isHighlightedPost: true,
626626- hasMore: false,
627627- isParentLoading: !!(record.value as AppBskyFeedPost.Record).reply,
628628- isChildLoading: true, // not available, so assume yes (to show the spinner)
629629- },
630630- }
631631-}
···11+import {createContext, useContext} from 'react'
22+33+import {
44+ type createPostThreadOtherQueryKey,
55+ type createPostThreadQueryKey,
66+} from '#/state/queries/usePostThread/types'
77+88+/**
99+ * Contains static metadata about the post thread query, suitable for
1010+ * context e.g. query keys and other things that don't update frequently.
1111+ *
1212+ * Be careful adding things here, as it could cause unnecessary re-renders.
1313+ */
1414+export type PostThreadContextType = {
1515+ postThreadQueryKey: ReturnType<typeof createPostThreadQueryKey>
1616+ postThreadOtherQueryKey: ReturnType<typeof createPostThreadOtherQueryKey>
1717+}
1818+1919+const PostThreadContext = createContext<PostThreadContextType | undefined>(
2020+ undefined,
2121+)
2222+2323+/**
2424+ * Use the current {@link PostThreadContext}, if one is available. If not,
2525+ * returns `undefined`.
2626+ */
2727+export function usePostThreadContext() {
2828+ return useContext(PostThreadContext)
2929+}
3030+3131+export function PostThreadContextProvider({
3232+ children,
3333+ context,
3434+}: {
3535+ children: React.ReactNode
3636+ context?: PostThreadContextType
3737+}) {
3838+ return (
3939+ <PostThreadContext.Provider value={context}>
4040+ {children}
4141+ </PostThreadContext.Provider>
4242+ )
4343+}