From 46b63accb8e73997f2a1bee24cfda220d29e048b Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 30 Nov 2023 21:35:58 +0000 Subject: [PATCH] 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 --- src/state/cache/post-shadow.ts | 118 +++++++++++------------ src/state/cache/profile-shadow.ts | 116 +++++++++++----------- src/state/queries/list-members.ts | 40 +++++++- src/state/queries/my-blocked-accounts.ts | 32 +++++- src/state/queries/my-muted-accounts.ts | 32 +++++- src/state/queries/notifications/feed.ts | 16 ++- src/state/queries/post-feed.ts | 30 +++++- src/state/queries/post-liked-by.ts | 32 +++++- src/state/queries/post-reposted-by.ts | 32 +++++- src/state/queries/post-thread.ts | 42 +++----- src/state/queries/profile-followers.ts | 32 +++++- src/state/queries/profile-follows.ts | 32 +++++- src/state/queries/profile.ts | 25 ++++- src/state/queries/suggested-follows.ts | 55 +++++++++++ 14 files changed, 462 insertions(+), 172 deletions(-) diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index b21bb712..e02d4f1e 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -1,12 +1,14 @@ -import {useEffect, useState, useMemo, useCallback} from 'react' +import {useEffect, useState, useMemo} from 'react' import EventEmitter from 'eventemitter3' import {AppBskyFeedDefs} from '@atproto/api' import {batchedUpdates} from '#/lib/batchedUpdates' import {Shadow, castAsShadow} from './types' +import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '../queries/notifications/feed' +import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '../queries/post-feed' +import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '../queries/post-thread' +import {queryClient} from 'lib/react-query' export type {Shadow} from './types' -const emitter = new EventEmitter() - export interface PostShadow { likeUri: string | undefined likeCount: number | undefined @@ -17,95 +19,83 @@ export interface PostShadow { export const POST_TOMBSTONE = Symbol('PostTombstone') -interface CacheEntry { - ts: number - value: PostShadow -} - -const firstSeenMap = new WeakMap() -function getFirstSeenTS(post: AppBskyFeedDefs.PostView): number { - let timeStamp = firstSeenMap.get(post) - if (timeStamp !== undefined) { - return timeStamp - } - timeStamp = Date.now() - firstSeenMap.set(post, timeStamp) - return timeStamp -} +const emitter = new EventEmitter() +const shadows: WeakMap< + AppBskyFeedDefs.PostView, + Partial +> = new WeakMap() export function usePostShadow( post: AppBskyFeedDefs.PostView, ): Shadow | typeof POST_TOMBSTONE { - const postSeenTS = getFirstSeenTS(post) - const [state, setState] = useState(() => ({ - ts: postSeenTS, - value: fromPost(post), - })) - + const [shadow, setShadow] = useState(() => shadows.get(post)) const [prevPost, setPrevPost] = useState(post) if (post !== prevPost) { - // if we got a new prop, assume it's fresher - // than whatever shadow state we accumulated setPrevPost(post) - setState({ - ts: postSeenTS, - value: fromPost(post), - }) + setShadow(shadows.get(post)) } - const onUpdate = useCallback( - (value: Partial) => { - setState(s => ({ts: Date.now(), value: {...s.value, ...value}})) - }, - [setState], - ) - - // react to shadow updates useEffect(() => { + function onUpdate() { + setShadow(shadows.get(post)) + } emitter.addListener(post.uri, onUpdate) return () => { emitter.removeListener(post.uri, onUpdate) } - }, [post.uri, onUpdate]) + }, [post, setShadow]) return useMemo(() => { - return state.ts > postSeenTS - ? mergeShadow(post, state.value) - : castAsShadow(post) - }, [post, state, postSeenTS]) -} - -export function updatePostShadow(uri: string, value: Partial) { - batchedUpdates(() => { - emitter.emit(uri, value) - }) -} - -function fromPost(post: AppBskyFeedDefs.PostView): PostShadow { - return { - likeUri: post.viewer?.like, - likeCount: post.likeCount, - repostUri: post.viewer?.repost, - repostCount: post.repostCount, - isDeleted: false, - } + if (shadow) { + return mergeShadow(post, shadow) + } else { + return castAsShadow(post) + } + }, [post, shadow]) } function mergeShadow( post: AppBskyFeedDefs.PostView, - shadow: PostShadow, + shadow: Partial, ): Shadow | typeof POST_TOMBSTONE { if (shadow.isDeleted) { return POST_TOMBSTONE } return castAsShadow({ ...post, - likeCount: shadow.likeCount, - repostCount: shadow.repostCount, + likeCount: 'likeCount' in shadow ? shadow.likeCount : post.likeCount, + repostCount: + 'repostCount' in shadow ? shadow.repostCount : post.repostCount, viewer: { ...(post.viewer || {}), - like: shadow.likeUri, - repost: shadow.repostUri, + like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, + repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost, }, }) } + +export function updatePostShadow(uri: string, value: Partial) { + const cachedPosts = findPostsInCache(uri) + for (let post of cachedPosts) { + shadows.set(post, {...shadows.get(post), ...value}) + } + batchedUpdates(() => { + emitter.emit(uri) + }) +} + +function* findPostsInCache( + uri: string, +): Generator { + for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { + yield post + } + for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { + yield post + } + for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { + if (node.type === 'post') { + yield node.post + } + } +} diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 6ebd3913..f85e1ad8 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -1,107 +1,101 @@ -import {useEffect, useState, useMemo, useCallback} from 'react' +import {useEffect, useState, useMemo} from 'react' import EventEmitter from 'eventemitter3' import {AppBskyActorDefs} from '@atproto/api' import {batchedUpdates} from '#/lib/batchedUpdates' +import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members' +import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '../queries/my-blocked-accounts' +import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '../queries/my-muted-accounts' +import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '../queries/post-liked-by' +import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '../queries/post-reposted-by' +import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '../queries/profile' +import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '../queries/profile-followers' +import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '../queries/profile-follows' +import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '../queries/suggested-follows' import {Shadow, castAsShadow} from './types' +import {queryClient} from 'lib/react-query' export type {Shadow} from './types' -const emitter = new EventEmitter() - export interface ProfileShadow { followingUri: string | undefined muted: boolean | undefined blockingUri: string | undefined } -interface CacheEntry { - ts: number - value: ProfileShadow -} - type ProfileView = | AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileViewDetailed -const firstSeenMap = new WeakMap() -function getFirstSeenTS(profile: ProfileView): number { - let timeStamp = firstSeenMap.get(profile) - if (timeStamp !== undefined) { - return timeStamp - } - timeStamp = Date.now() - firstSeenMap.set(profile, timeStamp) - return timeStamp -} +const shadows: WeakMap> = new WeakMap() +const emitter = new EventEmitter() export function useProfileShadow(profile: ProfileView): Shadow { - const profileSeenTS = getFirstSeenTS(profile) - const [state, setState] = useState(() => ({ - ts: profileSeenTS, - value: fromProfile(profile), - })) - - const [prevProfile, setPrevProfile] = useState(profile) - if (profile !== prevProfile) { - // if we got a new prop, assume it's fresher - // than whatever shadow state we accumulated - setPrevProfile(profile) - setState({ - ts: profileSeenTS, - value: fromProfile(profile), - }) + const [shadow, setShadow] = useState(() => shadows.get(profile)) + const [prevPost, setPrevPost] = useState(profile) + if (profile !== prevPost) { + setPrevPost(profile) + setShadow(shadows.get(profile)) } - const onUpdate = useCallback( - (value: Partial) => { - setState(s => ({ts: Date.now(), value: {...s.value, ...value}})) - }, - [setState], - ) - - // react to shadow updates useEffect(() => { + function onUpdate() { + setShadow(shadows.get(profile)) + } emitter.addListener(profile.did, onUpdate) return () => { emitter.removeListener(profile.did, onUpdate) } - }, [profile.did, onUpdate]) + }, [profile]) return useMemo(() => { - return state.ts > profileSeenTS - ? mergeShadow(profile, state.value) - : castAsShadow(profile) - }, [profile, state, profileSeenTS]) + if (shadow) { + return mergeShadow(profile, shadow) + } else { + return castAsShadow(profile) + } + }, [profile, shadow]) } export function updateProfileShadow( - uri: string, + did: string, value: Partial, ) { - batchedUpdates(() => { - emitter.emit(uri, value) - }) -} - -function fromProfile(profile: ProfileView): ProfileShadow { - return { - followingUri: profile.viewer?.following, - muted: profile.viewer?.muted, - blockingUri: profile.viewer?.blocking, + const cachedProfiles = findProfilesInCache(did) + for (let post of cachedProfiles) { + shadows.set(post, {...shadows.get(post), ...value}) } + batchedUpdates(() => { + emitter.emit(did, value) + }) } function mergeShadow( profile: ProfileView, - shadow: ProfileShadow, + shadow: Partial, ): Shadow { return castAsShadow({ ...profile, viewer: { ...(profile.viewer || {}), - following: shadow.followingUri, - muted: shadow.muted, - blocking: shadow.blockingUri, + following: + 'followingUri' in shadow + ? shadow.followingUri + : profile.viewer?.following, + muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted, + blocking: + 'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking, }, }) } + +function* findProfilesInCache(did: string): Generator { + yield* findAllProfilesInListMembersQueryData(queryClient, did) + yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did) + yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did) + yield* findAllProfilesInPostLikedByQueryData(queryClient, did) + yield* findAllProfilesInPostRepostedByQueryData(queryClient, did) + yield* findAllProfilesInProfileQueryData(queryClient, did) + yield* findAllProfilesInProfileFollowersQueryData(queryClient, did) + yield* findAllProfilesInProfileFollowsQueryData(queryClient, did) + yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) +} diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index 7aa91b1d..d84089c9 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -1,5 +1,10 @@ -import {AppBskyGraphGetList} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' import {STALE} from '#/state/queries' @@ -31,3 +36,34 @@ export function useListMembersQuery(uri: string) { getNextPageParam: lastPage => lastPage.cursor, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['list-members'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData) { + continue + } + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + if (page.list.creator.did === did) { + yield page.list.creator + } + for (const item of page.items) { + if (item.subject.did === did) { + yield item.subject + } + } + } + } + } +} diff --git a/src/state/queries/my-blocked-accounts.ts b/src/state/queries/my-blocked-accounts.ts index 2c099c63..badaaec3 100644 --- a/src/state/queries/my-blocked-accounts.ts +++ b/src/state/queries/my-blocked-accounts.ts @@ -1,5 +1,10 @@ -import {AppBskyGraphGetBlocks} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyGraphGetBlocks} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' @@ -26,3 +31,26 @@ export function useMyBlockedAccountsQuery() { getNextPageParam: lastPage => lastPage.cursor, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['my-blocked-accounts'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const block of page.blocks) { + if (block.did === did) { + yield block + } + } + } + } +} diff --git a/src/state/queries/my-muted-accounts.ts b/src/state/queries/my-muted-accounts.ts index a175931b..8929e04d 100644 --- a/src/state/queries/my-muted-accounts.ts +++ b/src/state/queries/my-muted-accounts.ts @@ -1,5 +1,10 @@ -import {AppBskyGraphGetMutes} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyGraphGetMutes} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' @@ -26,3 +31,26 @@ export function useMyMutedAccountsQuery() { getNextPageParam: lastPage => lastPage.cursor, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['my-muted-accounts'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const mute of page.mutes) { + if (mute.did === did) { + yield mute + } + } + } + } +} diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 5c519d04..0fd9a2fe 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -86,6 +86,19 @@ export function findPostInQueryData( queryClient: QueryClient, uri: string, ): AppBskyFeedDefs.PostView | undefined { + const generator = findAllPostsInQueryData(queryClient, uri) + const result = generator.next() + if (result.done) { + return undefined + } else { + return result.value + } +} + +export function* findAllPostsInQueryData( + queryClient: QueryClient, + uri: string, +): Generator { const queryDatas = queryClient.getQueriesData>({ queryKey: ['notification-feed'], }) @@ -96,10 +109,9 @@ export function findPostInQueryData( for (const page of queryData?.pages) { for (const item of page.items) { if (item.subject?.uri === uri) { - return item.subject + yield item.subject } } } } - return undefined } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index d87beb77..209f1f54 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -232,7 +232,20 @@ function createApi( export function findPostInQueryData( queryClient: QueryClient, uri: string, -): AppBskyFeedDefs.FeedViewPost | undefined { +): AppBskyFeedDefs.PostView | undefined { + const generator = findAllPostsInQueryData(queryClient, uri) + const result = generator.next() + if (result.done) { + return undefined + } else { + return result.value + } +} + +export function* findAllPostsInQueryData( + queryClient: QueryClient, + uri: string, +): Generator { const queryDatas = queryClient.getQueriesData< InfiniteData >({ @@ -245,12 +258,23 @@ export function findPostInQueryData( for (const page of queryData?.pages) { for (const item of page.feed) { if (item.post.uri === uri) { - return item + yield item.post + } + if ( + AppBskyFeedDefs.isPostView(item.reply?.parent) && + item.reply?.parent?.uri === uri + ) { + yield item.reply.parent + } + if ( + AppBskyFeedDefs.isPostView(item.reply?.root) && + item.reply?.root?.uri === uri + ) { + yield item.reply.root } } } } - return undefined } function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) { diff --git a/src/state/queries/post-liked-by.ts b/src/state/queries/post-liked-by.ts index 528b3be7..2cde07f2 100644 --- a/src/state/queries/post-liked-by.ts +++ b/src/state/queries/post-liked-by.ts @@ -1,5 +1,10 @@ -import {AppBskyFeedGetLikes} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyFeedGetLikes} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' @@ -31,3 +36,26 @@ export function usePostLikedByQuery(resolvedUri: string | undefined) { enabled: !!resolvedUri, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['post-liked-by'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const like of page.likes) { + if (like.actor.did === did) { + yield like.actor + } + } + } + } +} diff --git a/src/state/queries/post-reposted-by.ts b/src/state/queries/post-reposted-by.ts index f9a80056..db5fa651 100644 --- a/src/state/queries/post-reposted-by.ts +++ b/src/state/queries/post-reposted-by.ts @@ -1,5 +1,10 @@ -import {AppBskyFeedGetRepostedBy} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyFeedGetRepostedBy} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' @@ -31,3 +36,26 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) { enabled: !!resolvedUri, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['post-reposted-by'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const repostedBy of page.repostedBy) { + if (repostedBy.did === did) { + yield repostedBy + } + } + } + } +} diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index d40af1fe..cde45723 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -88,7 +88,7 @@ export function usePostThreadQuery(uri: string | undefined) { { const item = findPostInFeedQueryData(queryClient, uri) if (item) { - return feedViewPostToPlaceholderThread(item) + return postViewToPlaceholderThread(item) } } { @@ -213,6 +213,19 @@ function findPostInQueryData( queryClient: QueryClient, uri: string, ): ThreadNode | undefined { + const generator = findAllPostsInQueryData(queryClient, uri) + const result = generator.next() + if (result.done) { + return undefined + } else { + return result.value + } +} + +export function* findAllPostsInQueryData( + queryClient: QueryClient, + uri: string, +): Generator { const queryDatas = queryClient.getQueriesData({ queryKey: ['post-thread'], }) @@ -222,11 +235,10 @@ function findPostInQueryData( } for (const item of traverseThread(queryData)) { if (item.uri === uri) { - return item + yield item } } } - return undefined } function* traverseThread(node: ThreadNode): Generator { @@ -270,30 +282,6 @@ function threadNodeToPlaceholderThread( } } -function feedViewPostToPlaceholderThread( - item: AppBskyFeedDefs.FeedViewPost, -): ThreadNode { - return { - type: 'post', - _reactKey: item.post.uri, - uri: item.post.uri, - post: item.post, - record: item.post.record as AppBskyFeedPost.Record, // validated in post-feed - parent: undefined, - replies: undefined, - viewer: item.post.viewer, - ctx: { - depth: 0, - isHighlightedPost: true, - hasMore: false, - showChildReplyLine: false, - showParentReplyLine: false, - isParentLoading: !!(item.post.record as AppBskyFeedPost.Record).reply, - isChildLoading: !!item.post.replyCount, - }, - } -} - function postViewToPlaceholderThread( post: AppBskyFeedDefs.PostView, ): ThreadNode { diff --git a/src/state/queries/profile-followers.ts b/src/state/queries/profile-followers.ts index b2008851..fdefc825 100644 --- a/src/state/queries/profile-followers.ts +++ b/src/state/queries/profile-followers.ts @@ -1,5 +1,10 @@ -import {AppBskyGraphGetFollowers} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyGraphGetFollowers} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' @@ -30,3 +35,26 @@ export function useProfileFollowersQuery(did: string | undefined) { enabled: !!did, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['profile-followers'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const follower of page.followers) { + if (follower.did === did) { + yield follower + } + } + } + } +} diff --git a/src/state/queries/profile-follows.ts b/src/state/queries/profile-follows.ts index 8af1fba0..428c8aeb 100644 --- a/src/state/queries/profile-follows.ts +++ b/src/state/queries/profile-follows.ts @@ -1,5 +1,10 @@ -import {AppBskyGraphGetFollows} from '@atproto/api' -import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyActorDefs, AppBskyGraphGetFollows} from '@atproto/api' +import { + useInfiniteQuery, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/react-query' import {getAgent} from '#/state/session' import {STALE} from '#/state/queries' @@ -33,3 +38,26 @@ export function useProfileFollowsQuery(did: string | undefined) { enabled: !!did, }) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['profile-follows'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const follow of page.follows) { + if (follow.did === did) { + yield follow + } + } + } + } +} diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 62e8f39c..9435d7ad 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -5,7 +5,12 @@ import { AppBskyActorProfile, AppBskyActorGetProfile, } from '@atproto/api' -import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query' +import { + useQuery, + useQueryClient, + useMutation, + QueryClient, +} from '@tanstack/react-query' import {Image as RNImage} from 'react-native-image-crop-picker' import {useSession, getAgent} from '../session' import {updateProfileShadow} from '../cache/profile-shadow' @@ -477,3 +482,21 @@ async function whenAppViewReady( () => getAgent().app.bsky.actor.getProfile({actor}), ) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + const queryDatas = + queryClient.getQueriesData({ + queryKey: ['profile'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData) { + continue + } + if (queryData.did === did) { + yield queryData + } + } +} diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index eadcb590..932226b7 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -1,5 +1,6 @@ import React from 'react' import { + AppBskyActorDefs, AppBskyActorGetSuggestions, AppBskyGraphGetSuggestedFollowsByActor, moderateProfile, @@ -9,6 +10,7 @@ import { useQueryClient, useQuery, InfiniteData, + QueryClient, QueryKey, } from '@tanstack/react-query' @@ -106,3 +108,56 @@ export function useGetSuggestedFollowersByActor() { [queryClient], ) } + +export function* findAllProfilesInQueryData( + queryClient: QueryClient, + did: string, +): Generator { + yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) + yield* findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did) +} + +function* findAllProfilesInSuggestedFollowsQueryData( + queryClient: QueryClient, + did: string, +) { + const queryDatas = queryClient.getQueriesData< + InfiniteData + >({ + queryKey: ['suggested-follows'], + }) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData?.pages) { + continue + } + for (const page of queryData?.pages) { + for (const actor of page.actors) { + if (actor.did === did) { + yield actor + } + } + } + } +} + +function* findAllProfilesInSuggestedFollowsByActorQueryData( + queryClient: QueryClient, + did: string, +) { + const queryDatas = + queryClient.getQueriesData( + { + queryKey: ['suggested-follows-by-actor'], + }, + ) + for (const [_queryKey, queryData] of queryDatas) { + if (!queryData) { + continue + } + for (const suggestion of queryData.suggestions) { + if (suggestion.did === did) { + yield suggestion + } + } + } +}