mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at tooltip 300 lines 9.6 kB view raw
1import { 2 type $Typed, 3 type AppBskyActorDefs, 4 type AppBskyFeedDefs, 5 AppBskyUnspeccedDefs, 6 type AppBskyUnspeccedGetPostThreadOtherV2, 7 type AppBskyUnspeccedGetPostThreadV2, 8 AtUri, 9} from '@atproto/api' 10import {type QueryClient} from '@tanstack/react-query' 11 12import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews' 13import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' 14import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 15import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 16import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 17import {getBranch} from '#/state/queries/usePostThread/traversal' 18import { 19 type ApiThreadItem, 20 type createPostThreadOtherQueryKey, 21 type createPostThreadQueryKey, 22 type PostThreadParams, 23 postThreadQueryKeyRoot, 24} from '#/state/queries/usePostThread/types' 25import {getRootPostAtUri} from '#/state/queries/usePostThread/utils' 26import {postViewToThreadPlaceholder} from '#/state/queries/usePostThread/views' 27import {didOrHandleUriMatches, getEmbeddedPost} from '#/state/queries/util' 28import {embedViewRecordToPostView} from '#/state/queries/util' 29 30export function createCacheMutator({ 31 queryClient, 32 postThreadQueryKey, 33 postThreadOtherQueryKey, 34 params, 35}: { 36 queryClient: QueryClient 37 postThreadQueryKey: ReturnType<typeof createPostThreadQueryKey> 38 postThreadOtherQueryKey: ReturnType<typeof createPostThreadOtherQueryKey> 39 params: Pick<PostThreadParams, 'view'> & {below: number} 40}) { 41 return { 42 insertReplies( 43 parentUri: string, 44 replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[], 45 ) { 46 /* 47 * Main thread query mutator. 48 */ 49 queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>( 50 postThreadQueryKey, 51 data => { 52 if (!data) return 53 return { 54 ...data, 55 thread: mutator<AppBskyUnspeccedGetPostThreadV2.ThreadItem>([ 56 ...data.thread, 57 ]), 58 } 59 }, 60 ) 61 62 /* 63 * Additional replies query mutator. 64 */ 65 queryClient.setQueryData<AppBskyUnspeccedGetPostThreadOtherV2.OutputSchema>( 66 postThreadOtherQueryKey, 67 data => { 68 if (!data) return 69 return { 70 ...data, 71 thread: mutator<AppBskyUnspeccedGetPostThreadOtherV2.ThreadItem>([ 72 ...data.thread, 73 ]), 74 } 75 }, 76 ) 77 78 function mutator<T>(thread: ApiThreadItem[]): T[] { 79 for (let i = 0; i < thread.length; i++) { 80 const existingParent = thread[i] 81 if (!AppBskyUnspeccedDefs.isThreadItemPost(existingParent.value)) 82 continue 83 if (existingParent.uri !== parentUri) continue 84 85 /* 86 * Update parent data 87 */ 88 existingParent.value.post = { 89 ...existingParent.value.post, 90 replyCount: (existingParent.value.post.replyCount || 0) + 1, 91 } 92 93 const opDid = getRootPostAtUri(existingParent.value.post)?.host 94 const nextItem = thread.at(i + 1) 95 const isReplyToRoot = existingParent.depth === 0 96 const isEndOfReplyChain = 97 !nextItem || nextItem.depth <= existingParent.depth 98 const firstReply = replies.at(0) 99 const opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost( 100 firstReply?.value, 101 ) 102 ? opDid === firstReply.value.post.author.did 103 : false 104 105 /* 106 * Always insert replies if the following conditions are met. 107 */ 108 const shouldAlwaysInsertReplies = 109 isReplyToRoot || 110 params.view === 'tree' || 111 (params.view === 'linear' && isEndOfReplyChain) 112 /* 113 * Maybe insert replies if the replier is the OP and certain conditions are met 114 */ 115 const shouldReplaceWithOPReplies = 116 !isReplyToRoot && params.view === 'linear' && opIsReplier 117 118 if (shouldAlwaysInsertReplies || shouldReplaceWithOPReplies) { 119 const branch = getBranch(thread, i, existingParent.depth) 120 /* 121 * OP insertions replace other replies _in linear view_. 122 */ 123 const itemsToRemove = shouldReplaceWithOPReplies ? branch.length : 0 124 const itemsToInsert = replies 125 .map((r, ri) => { 126 r.depth = existingParent.depth + 1 + ri 127 return r 128 }) 129 .filter(r => { 130 // Filter out replies that are too deep for our UI 131 return r.depth <= params.below 132 }) 133 134 thread.splice(i + 1, itemsToRemove, ...itemsToInsert) 135 } 136 } 137 138 return thread as T[] 139 } 140 }, 141 /** 142 * Unused atm, post shadow does the trick, but it would be nice to clean up 143 * the whole sub-tree on deletes. 144 */ 145 deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) { 146 queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>( 147 postThreadQueryKey, 148 queryData => { 149 if (!queryData) return 150 151 const thread = [...queryData.thread] 152 153 for (let i = 0; i < thread.length; i++) { 154 const existingPost = thread[i] 155 if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue 156 157 if (existingPost.uri === post.uri) { 158 const branch = getBranch(thread, i, existingPost.depth) 159 thread.splice(branch.start, branch.length) 160 break 161 } 162 } 163 164 return { 165 ...queryData, 166 thread, 167 } 168 }, 169 ) 170 }, 171 } 172} 173 174export function getThreadPlaceholder( 175 queryClient: QueryClient, 176 uri: string, 177): $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItem> | void { 178 let partial 179 for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { 180 /* 181 * Currently, the backend doesn't send full post info in some cases (for 182 * example, for quoted posts). We use missing `likeCount` as a way to 183 * detect that. In the future, we should fix this on the backend, which 184 * will let us always stop on the first result. 185 * 186 * TODO can we send in feeds and quotes? 187 */ 188 const hasAllInfo = item.value.post.likeCount != null 189 if (hasAllInfo) { 190 return item 191 } else { 192 // Keep searching, we might still find a full post in the cache. 193 partial = item 194 } 195 } 196 return partial 197} 198 199export function* getThreadPlaceholderCandidates( 200 queryClient: QueryClient, 201 uri: string, 202): Generator< 203 $Typed< 204 Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & { 205 value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> 206 } 207 >, 208 void 209> { 210 /* 211 * Check post thread queries first 212 */ 213 for (const post of findAllPostsInQueryData(queryClient, uri)) { 214 yield postViewToThreadPlaceholder(post) 215 } 216 217 /* 218 * Check notifications first. If you have a post in notifications, it's 219 * often due to a like or a repost, and we want to prioritize a post object 220 * with >0 likes/reposts over a stale version with no metrics in order to 221 * avoid a notification->post scroll jump. 222 */ 223 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 224 yield postViewToThreadPlaceholder(post) 225 } 226 for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { 227 yield postViewToThreadPlaceholder(post) 228 } 229 for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { 230 yield postViewToThreadPlaceholder(post) 231 } 232 for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { 233 yield postViewToThreadPlaceholder(post) 234 } 235 for (let post of findAllPostsInExploreFeedPreviewsQueryData( 236 queryClient, 237 uri, 238 )) { 239 yield postViewToThreadPlaceholder(post) 240 } 241} 242 243export function* findAllPostsInQueryData( 244 queryClient: QueryClient, 245 uri: string, 246): Generator<AppBskyFeedDefs.PostView, void> { 247 const atUri = new AtUri(uri) 248 const queryDatas = 249 queryClient.getQueriesData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>({ 250 queryKey: [postThreadQueryKeyRoot], 251 }) 252 253 for (const [_queryKey, queryData] of queryDatas) { 254 if (!queryData) continue 255 256 const {thread} = queryData 257 258 for (const item of thread) { 259 if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { 260 if (didOrHandleUriMatches(atUri, item.value.post)) { 261 yield item.value.post 262 } 263 264 const qp = getEmbeddedPost(item.value.post.embed) 265 if (qp && didOrHandleUriMatches(atUri, qp)) { 266 yield embedViewRecordToPostView(qp) 267 } 268 } 269 } 270 } 271} 272 273export function* findAllProfilesInQueryData( 274 queryClient: QueryClient, 275 did: string, 276): Generator<AppBskyActorDefs.ProfileViewBasic, void> { 277 const queryDatas = 278 queryClient.getQueriesData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>({ 279 queryKey: [postThreadQueryKeyRoot], 280 }) 281 282 for (const [_queryKey, queryData] of queryDatas) { 283 if (!queryData) continue 284 285 const {thread} = queryData 286 287 for (const item of thread) { 288 if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { 289 if (item.value.post.author.did === did) { 290 yield item.value.post.author 291 } 292 293 const qp = getEmbeddedPost(item.value.post.embed) 294 if (qp && qp.author.did === did) { 295 yield qp.author 296 } 297 } 298 } 299 } 300}