mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rewrite the shadow logic to look inside the cache (#2045)

* Reset

* Associate shadows with the cache

* Use colocated helpers

* Fix types

* Reorder for clarity

* More types

* Copy paste logic for profile

* Hook up profile query

* Hook up suggested follows

* Hook up other profile things

* Fix shape

* Pass setShadow into the effect deps

* Include reply posts in the shadow cache search

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by danabra.mov

Paul Frazee and committed by
GitHub
46b63acc 143fc809

+461 -171
+54 -64
src/state/cache/post-shadow.ts
··· 1 - import {useEffect, useState, useMemo, useCallback} from 'react' 1 + import {useEffect, useState, useMemo} from 'react' 2 2 import EventEmitter from 'eventemitter3' 3 3 import {AppBskyFeedDefs} from '@atproto/api' 4 4 import {batchedUpdates} from '#/lib/batchedUpdates' 5 5 import {Shadow, castAsShadow} from './types' 6 + import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '../queries/notifications/feed' 7 + import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '../queries/post-feed' 8 + import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '../queries/post-thread' 9 + import {queryClient} from 'lib/react-query' 6 10 export type {Shadow} from './types' 7 - 8 - const emitter = new EventEmitter() 9 11 10 12 export interface PostShadow { 11 13 likeUri: string | undefined ··· 17 19 18 20 export const POST_TOMBSTONE = Symbol('PostTombstone') 19 21 20 - interface CacheEntry { 21 - ts: number 22 - value: PostShadow 23 - } 24 - 25 - const firstSeenMap = new WeakMap<AppBskyFeedDefs.PostView, number>() 26 - function getFirstSeenTS(post: AppBskyFeedDefs.PostView): number { 27 - let timeStamp = firstSeenMap.get(post) 28 - if (timeStamp !== undefined) { 29 - return timeStamp 30 - } 31 - timeStamp = Date.now() 32 - firstSeenMap.set(post, timeStamp) 33 - return timeStamp 34 - } 22 + const emitter = new EventEmitter() 23 + const shadows: WeakMap< 24 + AppBskyFeedDefs.PostView, 25 + Partial<PostShadow> 26 + > = new WeakMap() 35 27 36 28 export function usePostShadow( 37 29 post: AppBskyFeedDefs.PostView, 38 30 ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 39 - const postSeenTS = getFirstSeenTS(post) 40 - const [state, setState] = useState<CacheEntry>(() => ({ 41 - ts: postSeenTS, 42 - value: fromPost(post), 43 - })) 44 - 31 + const [shadow, setShadow] = useState(() => shadows.get(post)) 45 32 const [prevPost, setPrevPost] = useState(post) 46 33 if (post !== prevPost) { 47 - // if we got a new prop, assume it's fresher 48 - // than whatever shadow state we accumulated 49 34 setPrevPost(post) 50 - setState({ 51 - ts: postSeenTS, 52 - value: fromPost(post), 53 - }) 35 + setShadow(shadows.get(post)) 54 36 } 55 37 56 - const onUpdate = useCallback( 57 - (value: Partial<PostShadow>) => { 58 - setState(s => ({ts: Date.now(), value: {...s.value, ...value}})) 59 - }, 60 - [setState], 61 - ) 62 - 63 - // react to shadow updates 64 38 useEffect(() => { 39 + function onUpdate() { 40 + setShadow(shadows.get(post)) 41 + } 65 42 emitter.addListener(post.uri, onUpdate) 66 43 return () => { 67 44 emitter.removeListener(post.uri, onUpdate) 68 45 } 69 - }, [post.uri, onUpdate]) 46 + }, [post, setShadow]) 70 47 71 48 return useMemo(() => { 72 - return state.ts > postSeenTS 73 - ? mergeShadow(post, state.value) 74 - : castAsShadow(post) 75 - }, [post, state, postSeenTS]) 76 - } 77 - 78 - export function updatePostShadow(uri: string, value: Partial<PostShadow>) { 79 - batchedUpdates(() => { 80 - emitter.emit(uri, value) 81 - }) 82 - } 83 - 84 - function fromPost(post: AppBskyFeedDefs.PostView): PostShadow { 85 - return { 86 - likeUri: post.viewer?.like, 87 - likeCount: post.likeCount, 88 - repostUri: post.viewer?.repost, 89 - repostCount: post.repostCount, 90 - isDeleted: false, 91 - } 49 + if (shadow) { 50 + return mergeShadow(post, shadow) 51 + } else { 52 + return castAsShadow(post) 53 + } 54 + }, [post, shadow]) 92 55 } 93 56 94 57 function mergeShadow( 95 58 post: AppBskyFeedDefs.PostView, 96 - shadow: PostShadow, 59 + shadow: Partial<PostShadow>, 97 60 ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 98 61 if (shadow.isDeleted) { 99 62 return POST_TOMBSTONE 100 63 } 101 64 return castAsShadow({ 102 65 ...post, 103 - likeCount: shadow.likeCount, 104 - repostCount: shadow.repostCount, 66 + likeCount: 'likeCount' in shadow ? shadow.likeCount : post.likeCount, 67 + repostCount: 68 + 'repostCount' in shadow ? shadow.repostCount : post.repostCount, 105 69 viewer: { 106 70 ...(post.viewer || {}), 107 - like: shadow.likeUri, 108 - repost: shadow.repostUri, 71 + like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, 72 + repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost, 109 73 }, 110 74 }) 111 75 } 76 + 77 + export function updatePostShadow(uri: string, value: Partial<PostShadow>) { 78 + const cachedPosts = findPostsInCache(uri) 79 + for (let post of cachedPosts) { 80 + shadows.set(post, {...shadows.get(post), ...value}) 81 + } 82 + batchedUpdates(() => { 83 + emitter.emit(uri) 84 + }) 85 + } 86 + 87 + function* findPostsInCache( 88 + uri: string, 89 + ): Generator<AppBskyFeedDefs.PostView, void> { 90 + for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { 91 + yield post 92 + } 93 + for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 94 + yield post 95 + } 96 + for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { 97 + if (node.type === 'post') { 98 + yield node.post 99 + } 100 + } 101 + }
+54 -60
src/state/cache/profile-shadow.ts
··· 1 - import {useEffect, useState, useMemo, useCallback} from 'react' 1 + import {useEffect, useState, useMemo} from 'react' 2 2 import EventEmitter from 'eventemitter3' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 4 import {batchedUpdates} from '#/lib/batchedUpdates' 5 + import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members' 6 + import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '../queries/my-blocked-accounts' 7 + import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '../queries/my-muted-accounts' 8 + import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '../queries/post-liked-by' 9 + import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '../queries/post-reposted-by' 10 + import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '../queries/profile' 11 + import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '../queries/profile-followers' 12 + import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '../queries/profile-follows' 13 + import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '../queries/suggested-follows' 5 14 import {Shadow, castAsShadow} from './types' 15 + import {queryClient} from 'lib/react-query' 6 16 export type {Shadow} from './types' 7 - 8 - const emitter = new EventEmitter() 9 17 10 18 export interface ProfileShadow { 11 19 followingUri: string | undefined ··· 13 21 blockingUri: string | undefined 14 22 } 15 23 16 - interface CacheEntry { 17 - ts: number 18 - value: ProfileShadow 19 - } 20 - 21 24 type ProfileView = 22 25 | AppBskyActorDefs.ProfileView 23 26 | AppBskyActorDefs.ProfileViewBasic 24 27 | AppBskyActorDefs.ProfileViewDetailed 25 28 26 - const firstSeenMap = new WeakMap<ProfileView, number>() 27 - function getFirstSeenTS(profile: ProfileView): number { 28 - let timeStamp = firstSeenMap.get(profile) 29 - if (timeStamp !== undefined) { 30 - return timeStamp 31 - } 32 - timeStamp = Date.now() 33 - firstSeenMap.set(profile, timeStamp) 34 - return timeStamp 35 - } 29 + const shadows: WeakMap<ProfileView, Partial<ProfileShadow>> = new WeakMap() 30 + const emitter = new EventEmitter() 36 31 37 32 export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> { 38 - const profileSeenTS = getFirstSeenTS(profile) 39 - const [state, setState] = useState<CacheEntry>(() => ({ 40 - ts: profileSeenTS, 41 - value: fromProfile(profile), 42 - })) 43 - 44 - const [prevProfile, setPrevProfile] = useState(profile) 45 - if (profile !== prevProfile) { 46 - // if we got a new prop, assume it's fresher 47 - // than whatever shadow state we accumulated 48 - setPrevProfile(profile) 49 - setState({ 50 - ts: profileSeenTS, 51 - value: fromProfile(profile), 52 - }) 33 + const [shadow, setShadow] = useState(() => shadows.get(profile)) 34 + const [prevPost, setPrevPost] = useState(profile) 35 + if (profile !== prevPost) { 36 + setPrevPost(profile) 37 + setShadow(shadows.get(profile)) 53 38 } 54 39 55 - const onUpdate = useCallback( 56 - (value: Partial<ProfileShadow>) => { 57 - setState(s => ({ts: Date.now(), value: {...s.value, ...value}})) 58 - }, 59 - [setState], 60 - ) 61 - 62 - // react to shadow updates 63 40 useEffect(() => { 41 + function onUpdate() { 42 + setShadow(shadows.get(profile)) 43 + } 64 44 emitter.addListener(profile.did, onUpdate) 65 45 return () => { 66 46 emitter.removeListener(profile.did, onUpdate) 67 47 } 68 - }, [profile.did, onUpdate]) 48 + }, [profile]) 69 49 70 50 return useMemo(() => { 71 - return state.ts > profileSeenTS 72 - ? mergeShadow(profile, state.value) 73 - : castAsShadow(profile) 74 - }, [profile, state, profileSeenTS]) 51 + if (shadow) { 52 + return mergeShadow(profile, shadow) 53 + } else { 54 + return castAsShadow(profile) 55 + } 56 + }, [profile, shadow]) 75 57 } 76 58 77 59 export function updateProfileShadow( 78 - uri: string, 60 + did: string, 79 61 value: Partial<ProfileShadow>, 80 62 ) { 63 + const cachedProfiles = findProfilesInCache(did) 64 + for (let post of cachedProfiles) { 65 + shadows.set(post, {...shadows.get(post), ...value}) 66 + } 81 67 batchedUpdates(() => { 82 - emitter.emit(uri, value) 68 + emitter.emit(did, value) 83 69 }) 84 70 } 85 71 86 - function fromProfile(profile: ProfileView): ProfileShadow { 87 - return { 88 - followingUri: profile.viewer?.following, 89 - muted: profile.viewer?.muted, 90 - blockingUri: profile.viewer?.blocking, 91 - } 92 - } 93 - 94 72 function mergeShadow( 95 73 profile: ProfileView, 96 - shadow: ProfileShadow, 74 + shadow: Partial<ProfileShadow>, 97 75 ): Shadow<ProfileView> { 98 76 return castAsShadow({ 99 77 ...profile, 100 78 viewer: { 101 79 ...(profile.viewer || {}), 102 - following: shadow.followingUri, 103 - muted: shadow.muted, 104 - blocking: shadow.blockingUri, 80 + following: 81 + 'followingUri' in shadow 82 + ? shadow.followingUri 83 + : profile.viewer?.following, 84 + muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted, 85 + blocking: 86 + 'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking, 105 87 }, 106 88 }) 107 89 } 90 + 91 + function* findProfilesInCache(did: string): Generator<ProfileView, void> { 92 + yield* findAllProfilesInListMembersQueryData(queryClient, did) 93 + yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did) 94 + yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did) 95 + yield* findAllProfilesInPostLikedByQueryData(queryClient, did) 96 + yield* findAllProfilesInPostRepostedByQueryData(queryClient, did) 97 + yield* findAllProfilesInProfileQueryData(queryClient, did) 98 + yield* findAllProfilesInProfileFollowersQueryData(queryClient, did) 99 + yield* findAllProfilesInProfileFollowsQueryData(queryClient, did) 100 + yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) 101 + }
+38 -2
src/state/queries/list-members.ts
··· 1 - import {AppBskyGraphGetList} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 import {STALE} from '#/state/queries' ··· 31 36 getNextPageParam: lastPage => lastPage.cursor, 32 37 }) 33 38 } 39 + 40 + export function* findAllProfilesInQueryData( 41 + queryClient: QueryClient, 42 + did: string, 43 + ): Generator<AppBskyActorDefs.ProfileView, void> { 44 + const queryDatas = queryClient.getQueriesData< 45 + InfiniteData<AppBskyGraphGetList.OutputSchema> 46 + >({ 47 + queryKey: ['list-members'], 48 + }) 49 + for (const [_queryKey, queryData] of queryDatas) { 50 + if (!queryData) { 51 + continue 52 + } 53 + for (const [_queryKey, queryData] of queryDatas) { 54 + if (!queryData?.pages) { 55 + continue 56 + } 57 + for (const page of queryData?.pages) { 58 + if (page.list.creator.did === did) { 59 + yield page.list.creator 60 + } 61 + for (const item of page.items) { 62 + if (item.subject.did === did) { 63 + yield item.subject 64 + } 65 + } 66 + } 67 + } 68 + } 69 + }
+30 -2
src/state/queries/my-blocked-accounts.ts
··· 1 - import {AppBskyGraphGetBlocks} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyGraphGetBlocks} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 ··· 26 31 getNextPageParam: lastPage => lastPage.cursor, 27 32 }) 28 33 } 34 + 35 + export function* findAllProfilesInQueryData( 36 + queryClient: QueryClient, 37 + did: string, 38 + ): Generator<AppBskyActorDefs.ProfileView, void> { 39 + const queryDatas = queryClient.getQueriesData< 40 + InfiniteData<AppBskyGraphGetBlocks.OutputSchema> 41 + >({ 42 + queryKey: ['my-blocked-accounts'], 43 + }) 44 + for (const [_queryKey, queryData] of queryDatas) { 45 + if (!queryData?.pages) { 46 + continue 47 + } 48 + for (const page of queryData?.pages) { 49 + for (const block of page.blocks) { 50 + if (block.did === did) { 51 + yield block 52 + } 53 + } 54 + } 55 + } 56 + }
+30 -2
src/state/queries/my-muted-accounts.ts
··· 1 - import {AppBskyGraphGetMutes} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyGraphGetMutes} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 ··· 26 31 getNextPageParam: lastPage => lastPage.cursor, 27 32 }) 28 33 } 34 + 35 + export function* findAllProfilesInQueryData( 36 + queryClient: QueryClient, 37 + did: string, 38 + ): Generator<AppBskyActorDefs.ProfileView, void> { 39 + const queryDatas = queryClient.getQueriesData< 40 + InfiniteData<AppBskyGraphGetMutes.OutputSchema> 41 + >({ 42 + queryKey: ['my-muted-accounts'], 43 + }) 44 + for (const [_queryKey, queryData] of queryDatas) { 45 + if (!queryData?.pages) { 46 + continue 47 + } 48 + for (const page of queryData?.pages) { 49 + for (const mute of page.mutes) { 50 + if (mute.did === did) { 51 + yield mute 52 + } 53 + } 54 + } 55 + } 56 + }
+14 -2
src/state/queries/notifications/feed.ts
··· 86 86 queryClient: QueryClient, 87 87 uri: string, 88 88 ): AppBskyFeedDefs.PostView | undefined { 89 + const generator = findAllPostsInQueryData(queryClient, uri) 90 + const result = generator.next() 91 + if (result.done) { 92 + return undefined 93 + } else { 94 + return result.value 95 + } 96 + } 97 + 98 + export function* findAllPostsInQueryData( 99 + queryClient: QueryClient, 100 + uri: string, 101 + ): Generator<AppBskyFeedDefs.PostView, void> { 89 102 const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({ 90 103 queryKey: ['notification-feed'], 91 104 }) ··· 96 109 for (const page of queryData?.pages) { 97 110 for (const item of page.items) { 98 111 if (item.subject?.uri === uri) { 99 - return item.subject 112 + yield item.subject 100 113 } 101 114 } 102 115 } 103 116 } 104 - return undefined 105 117 }
+27 -3
src/state/queries/post-feed.ts
··· 232 232 export function findPostInQueryData( 233 233 queryClient: QueryClient, 234 234 uri: string, 235 - ): AppBskyFeedDefs.FeedViewPost | undefined { 235 + ): AppBskyFeedDefs.PostView | undefined { 236 + const generator = findAllPostsInQueryData(queryClient, uri) 237 + const result = generator.next() 238 + if (result.done) { 239 + return undefined 240 + } else { 241 + return result.value 242 + } 243 + } 244 + 245 + export function* findAllPostsInQueryData( 246 + queryClient: QueryClient, 247 + uri: string, 248 + ): Generator<AppBskyFeedDefs.PostView, void> { 236 249 const queryDatas = queryClient.getQueriesData< 237 250 InfiniteData<FeedPageUnselected> 238 251 >({ ··· 245 258 for (const page of queryData?.pages) { 246 259 for (const item of page.feed) { 247 260 if (item.post.uri === uri) { 248 - return item 261 + yield item.post 262 + } 263 + if ( 264 + AppBskyFeedDefs.isPostView(item.reply?.parent) && 265 + item.reply?.parent?.uri === uri 266 + ) { 267 + yield item.reply.parent 268 + } 269 + if ( 270 + AppBskyFeedDefs.isPostView(item.reply?.root) && 271 + item.reply?.root?.uri === uri 272 + ) { 273 + yield item.reply.root 249 274 } 250 275 } 251 276 } 252 277 } 253 - return undefined 254 278 } 255 279 256 280 function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
+30 -2
src/state/queries/post-liked-by.ts
··· 1 - import {AppBskyFeedGetLikes} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyFeedGetLikes} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 ··· 31 36 enabled: !!resolvedUri, 32 37 }) 33 38 } 39 + 40 + export function* findAllProfilesInQueryData( 41 + queryClient: QueryClient, 42 + did: string, 43 + ): Generator<AppBskyActorDefs.ProfileView, void> { 44 + const queryDatas = queryClient.getQueriesData< 45 + InfiniteData<AppBskyFeedGetLikes.OutputSchema> 46 + >({ 47 + queryKey: ['post-liked-by'], 48 + }) 49 + for (const [_queryKey, queryData] of queryDatas) { 50 + if (!queryData?.pages) { 51 + continue 52 + } 53 + for (const page of queryData?.pages) { 54 + for (const like of page.likes) { 55 + if (like.actor.did === did) { 56 + yield like.actor 57 + } 58 + } 59 + } 60 + } 61 + }
+30 -2
src/state/queries/post-reposted-by.ts
··· 1 - import {AppBskyFeedGetRepostedBy} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyFeedGetRepostedBy} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 ··· 31 36 enabled: !!resolvedUri, 32 37 }) 33 38 } 39 + 40 + export function* findAllProfilesInQueryData( 41 + queryClient: QueryClient, 42 + did: string, 43 + ): Generator<AppBskyActorDefs.ProfileView, void> { 44 + const queryDatas = queryClient.getQueriesData< 45 + InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema> 46 + >({ 47 + queryKey: ['post-reposted-by'], 48 + }) 49 + for (const [_queryKey, queryData] of queryDatas) { 50 + if (!queryData?.pages) { 51 + continue 52 + } 53 + for (const page of queryData?.pages) { 54 + for (const repostedBy of page.repostedBy) { 55 + if (repostedBy.did === did) { 56 + yield repostedBy 57 + } 58 + } 59 + } 60 + } 61 + }
+15 -27
src/state/queries/post-thread.ts
··· 88 88 { 89 89 const item = findPostInFeedQueryData(queryClient, uri) 90 90 if (item) { 91 - return feedViewPostToPlaceholderThread(item) 91 + return postViewToPlaceholderThread(item) 92 92 } 93 93 } 94 94 { ··· 213 213 queryClient: QueryClient, 214 214 uri: string, 215 215 ): ThreadNode | undefined { 216 + const generator = findAllPostsInQueryData(queryClient, uri) 217 + const result = generator.next() 218 + if (result.done) { 219 + return undefined 220 + } else { 221 + return result.value 222 + } 223 + } 224 + 225 + export function* findAllPostsInQueryData( 226 + queryClient: QueryClient, 227 + uri: string, 228 + ): Generator<ThreadNode, void> { 216 229 const queryDatas = queryClient.getQueriesData<ThreadNode>({ 217 230 queryKey: ['post-thread'], 218 231 }) ··· 222 235 } 223 236 for (const item of traverseThread(queryData)) { 224 237 if (item.uri === uri) { 225 - return item 238 + yield item 226 239 } 227 240 } 228 241 } 229 - return undefined 230 242 } 231 243 232 244 function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> { ··· 266 278 showParentReplyLine: false, 267 279 isParentLoading: !!node.record.reply, 268 280 isChildLoading: !!node.post.replyCount, 269 - }, 270 - } 271 - } 272 - 273 - function feedViewPostToPlaceholderThread( 274 - item: AppBskyFeedDefs.FeedViewPost, 275 - ): ThreadNode { 276 - return { 277 - type: 'post', 278 - _reactKey: item.post.uri, 279 - uri: item.post.uri, 280 - post: item.post, 281 - record: item.post.record as AppBskyFeedPost.Record, // validated in post-feed 282 - parent: undefined, 283 - replies: undefined, 284 - viewer: item.post.viewer, 285 - ctx: { 286 - depth: 0, 287 - isHighlightedPost: true, 288 - hasMore: false, 289 - showChildReplyLine: false, 290 - showParentReplyLine: false, 291 - isParentLoading: !!(item.post.record as AppBskyFeedPost.Record).reply, 292 - isChildLoading: !!item.post.replyCount, 293 281 }, 294 282 } 295 283 }
+30 -2
src/state/queries/profile-followers.ts
··· 1 - import {AppBskyGraphGetFollowers} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyGraphGetFollowers} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 ··· 30 35 enabled: !!did, 31 36 }) 32 37 } 38 + 39 + export function* findAllProfilesInQueryData( 40 + queryClient: QueryClient, 41 + did: string, 42 + ): Generator<AppBskyActorDefs.ProfileView, void> { 43 + const queryDatas = queryClient.getQueriesData< 44 + InfiniteData<AppBskyGraphGetFollowers.OutputSchema> 45 + >({ 46 + queryKey: ['profile-followers'], 47 + }) 48 + for (const [_queryKey, queryData] of queryDatas) { 49 + if (!queryData?.pages) { 50 + continue 51 + } 52 + for (const page of queryData?.pages) { 53 + for (const follower of page.followers) { 54 + if (follower.did === did) { 55 + yield follower 56 + } 57 + } 58 + } 59 + } 60 + }
+30 -2
src/state/queries/profile-follows.ts
··· 1 - import {AppBskyGraphGetFollows} from '@atproto/api' 2 - import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' 1 + import {AppBskyActorDefs, AppBskyGraphGetFollows} from '@atproto/api' 2 + import { 3 + useInfiniteQuery, 4 + InfiniteData, 5 + QueryClient, 6 + QueryKey, 7 + } from '@tanstack/react-query' 3 8 4 9 import {getAgent} from '#/state/session' 5 10 import {STALE} from '#/state/queries' ··· 33 38 enabled: !!did, 34 39 }) 35 40 } 41 + 42 + export function* findAllProfilesInQueryData( 43 + queryClient: QueryClient, 44 + did: string, 45 + ): Generator<AppBskyActorDefs.ProfileView, void> { 46 + const queryDatas = queryClient.getQueriesData< 47 + InfiniteData<AppBskyGraphGetFollows.OutputSchema> 48 + >({ 49 + queryKey: ['profile-follows'], 50 + }) 51 + for (const [_queryKey, queryData] of queryDatas) { 52 + if (!queryData?.pages) { 53 + continue 54 + } 55 + for (const page of queryData?.pages) { 56 + for (const follow of page.follows) { 57 + if (follow.did === did) { 58 + yield follow 59 + } 60 + } 61 + } 62 + } 63 + }
+24 -1
src/state/queries/profile.ts
··· 5 5 AppBskyActorProfile, 6 6 AppBskyActorGetProfile, 7 7 } from '@atproto/api' 8 - import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query' 8 + import { 9 + useQuery, 10 + useQueryClient, 11 + useMutation, 12 + QueryClient, 13 + } from '@tanstack/react-query' 9 14 import {Image as RNImage} from 'react-native-image-crop-picker' 10 15 import {useSession, getAgent} from '../session' 11 16 import {updateProfileShadow} from '../cache/profile-shadow' ··· 477 482 () => getAgent().app.bsky.actor.getProfile({actor}), 478 483 ) 479 484 } 485 + 486 + export function* findAllProfilesInQueryData( 487 + queryClient: QueryClient, 488 + did: string, 489 + ): Generator<AppBskyActorDefs.ProfileViewDetailed, void> { 490 + const queryDatas = 491 + queryClient.getQueriesData<AppBskyActorDefs.ProfileViewDetailed>({ 492 + queryKey: ['profile'], 493 + }) 494 + for (const [_queryKey, queryData] of queryDatas) { 495 + if (!queryData) { 496 + continue 497 + } 498 + if (queryData.did === did) { 499 + yield queryData 500 + } 501 + } 502 + }
+55
src/state/queries/suggested-follows.ts
··· 1 1 import React from 'react' 2 2 import { 3 + AppBskyActorDefs, 3 4 AppBskyActorGetSuggestions, 4 5 AppBskyGraphGetSuggestedFollowsByActor, 5 6 moderateProfile, ··· 9 10 useQueryClient, 10 11 useQuery, 11 12 InfiniteData, 13 + QueryClient, 12 14 QueryKey, 13 15 } from '@tanstack/react-query' 14 16 ··· 106 108 [queryClient], 107 109 ) 108 110 } 111 + 112 + export function* findAllProfilesInQueryData( 113 + queryClient: QueryClient, 114 + did: string, 115 + ): Generator<AppBskyActorDefs.ProfileView, void> { 116 + yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) 117 + yield* findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did) 118 + } 119 + 120 + function* findAllProfilesInSuggestedFollowsQueryData( 121 + queryClient: QueryClient, 122 + did: string, 123 + ) { 124 + const queryDatas = queryClient.getQueriesData< 125 + InfiniteData<AppBskyActorGetSuggestions.OutputSchema> 126 + >({ 127 + queryKey: ['suggested-follows'], 128 + }) 129 + for (const [_queryKey, queryData] of queryDatas) { 130 + if (!queryData?.pages) { 131 + continue 132 + } 133 + for (const page of queryData?.pages) { 134 + for (const actor of page.actors) { 135 + if (actor.did === did) { 136 + yield actor 137 + } 138 + } 139 + } 140 + } 141 + } 142 + 143 + function* findAllProfilesInSuggestedFollowsByActorQueryData( 144 + queryClient: QueryClient, 145 + did: string, 146 + ) { 147 + const queryDatas = 148 + queryClient.getQueriesData<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema>( 149 + { 150 + queryKey: ['suggested-follows-by-actor'], 151 + }, 152 + ) 153 + for (const [_queryKey, queryData] of queryDatas) { 154 + if (!queryData) { 155 + continue 156 + } 157 + for (const suggestion of queryData.suggestions) { 158 + if (suggestion.did === did) { 159 + yield suggestion 160 + } 161 + } 162 + } 163 + }