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>
This commit is contained in:
dan 2023-11-30 21:35:58 +00:00 committed by GitHub
parent 143fc80951
commit 46b63accb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 462 additions and 172 deletions

View file

@ -1,12 +1,14 @@
import {useEffect, useState, useMemo, useCallback} from 'react' import {useEffect, useState, useMemo} from 'react'
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
import {AppBskyFeedDefs} from '@atproto/api' import {AppBskyFeedDefs} from '@atproto/api'
import {batchedUpdates} from '#/lib/batchedUpdates' import {batchedUpdates} from '#/lib/batchedUpdates'
import {Shadow, castAsShadow} from './types' 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' export type {Shadow} from './types'
const emitter = new EventEmitter()
export interface PostShadow { export interface PostShadow {
likeUri: string | undefined likeUri: string | undefined
likeCount: number | undefined likeCount: number | undefined
@ -17,95 +19,83 @@ export interface PostShadow {
export const POST_TOMBSTONE = Symbol('PostTombstone') export const POST_TOMBSTONE = Symbol('PostTombstone')
interface CacheEntry { const emitter = new EventEmitter()
ts: number const shadows: WeakMap<
value: PostShadow AppBskyFeedDefs.PostView,
} Partial<PostShadow>
> = new WeakMap()
const firstSeenMap = new WeakMap<AppBskyFeedDefs.PostView, number>()
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
}
export function usePostShadow( export function usePostShadow(
post: AppBskyFeedDefs.PostView, post: AppBskyFeedDefs.PostView,
): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
const postSeenTS = getFirstSeenTS(post) const [shadow, setShadow] = useState(() => shadows.get(post))
const [state, setState] = useState<CacheEntry>(() => ({
ts: postSeenTS,
value: fromPost(post),
}))
const [prevPost, setPrevPost] = useState(post) const [prevPost, setPrevPost] = useState(post)
if (post !== prevPost) { if (post !== prevPost) {
// if we got a new prop, assume it's fresher
// than whatever shadow state we accumulated
setPrevPost(post) setPrevPost(post)
setState({ setShadow(shadows.get(post))
ts: postSeenTS,
value: fromPost(post),
})
} }
const onUpdate = useCallback(
(value: Partial<PostShadow>) => {
setState(s => ({ts: Date.now(), value: {...s.value, ...value}}))
},
[setState],
)
// react to shadow updates
useEffect(() => { useEffect(() => {
function onUpdate() {
setShadow(shadows.get(post))
}
emitter.addListener(post.uri, onUpdate) emitter.addListener(post.uri, onUpdate)
return () => { return () => {
emitter.removeListener(post.uri, onUpdate) emitter.removeListener(post.uri, onUpdate)
} }
}, [post.uri, onUpdate]) }, [post, setShadow])
return useMemo(() => { return useMemo(() => {
return state.ts > postSeenTS if (shadow) {
? mergeShadow(post, state.value) return mergeShadow(post, shadow)
: castAsShadow(post) } else {
}, [post, state, postSeenTS]) return castAsShadow(post)
}
export function updatePostShadow(uri: string, value: Partial<PostShadow>) {
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,
} }
}, [post, shadow])
} }
function mergeShadow( function mergeShadow(
post: AppBskyFeedDefs.PostView, post: AppBskyFeedDefs.PostView,
shadow: PostShadow, shadow: Partial<PostShadow>,
): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
if (shadow.isDeleted) { if (shadow.isDeleted) {
return POST_TOMBSTONE return POST_TOMBSTONE
} }
return castAsShadow({ return castAsShadow({
...post, ...post,
likeCount: shadow.likeCount, likeCount: 'likeCount' in shadow ? shadow.likeCount : post.likeCount,
repostCount: shadow.repostCount, repostCount:
'repostCount' in shadow ? shadow.repostCount : post.repostCount,
viewer: { viewer: {
...(post.viewer || {}), ...(post.viewer || {}),
like: shadow.likeUri, like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like,
repost: shadow.repostUri, repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost,
}, },
}) })
} }
export function updatePostShadow(uri: string, value: Partial<PostShadow>) {
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<AppBskyFeedDefs.PostView, void> {
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
}
}
}

View file

@ -1,107 +1,101 @@
import {useEffect, useState, useMemo, useCallback} from 'react' import {useEffect, useState, useMemo} from 'react'
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {batchedUpdates} from '#/lib/batchedUpdates' 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 {Shadow, castAsShadow} from './types'
import {queryClient} from 'lib/react-query'
export type {Shadow} from './types' export type {Shadow} from './types'
const emitter = new EventEmitter()
export interface ProfileShadow { export interface ProfileShadow {
followingUri: string | undefined followingUri: string | undefined
muted: boolean | undefined muted: boolean | undefined
blockingUri: string | undefined blockingUri: string | undefined
} }
interface CacheEntry {
ts: number
value: ProfileShadow
}
type ProfileView = type ProfileView =
| AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed | AppBskyActorDefs.ProfileViewDetailed
const firstSeenMap = new WeakMap<ProfileView, number>() const shadows: WeakMap<ProfileView, Partial<ProfileShadow>> = new WeakMap()
function getFirstSeenTS(profile: ProfileView): number { const emitter = new EventEmitter()
let timeStamp = firstSeenMap.get(profile)
if (timeStamp !== undefined) {
return timeStamp
}
timeStamp = Date.now()
firstSeenMap.set(profile, timeStamp)
return timeStamp
}
export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> { export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
const profileSeenTS = getFirstSeenTS(profile) const [shadow, setShadow] = useState(() => shadows.get(profile))
const [state, setState] = useState<CacheEntry>(() => ({ const [prevPost, setPrevPost] = useState(profile)
ts: profileSeenTS, if (profile !== prevPost) {
value: fromProfile(profile), setPrevPost(profile)
})) setShadow(shadows.get(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 onUpdate = useCallback(
(value: Partial<ProfileShadow>) => {
setState(s => ({ts: Date.now(), value: {...s.value, ...value}}))
},
[setState],
)
// react to shadow updates
useEffect(() => { useEffect(() => {
function onUpdate() {
setShadow(shadows.get(profile))
}
emitter.addListener(profile.did, onUpdate) emitter.addListener(profile.did, onUpdate)
return () => { return () => {
emitter.removeListener(profile.did, onUpdate) emitter.removeListener(profile.did, onUpdate)
} }
}, [profile.did, onUpdate]) }, [profile])
return useMemo(() => { return useMemo(() => {
return state.ts > profileSeenTS if (shadow) {
? mergeShadow(profile, state.value) return mergeShadow(profile, shadow)
: castAsShadow(profile) } else {
}, [profile, state, profileSeenTS]) return castAsShadow(profile)
}
}, [profile, shadow])
} }
export function updateProfileShadow( export function updateProfileShadow(
uri: string, did: string,
value: Partial<ProfileShadow>, value: Partial<ProfileShadow>,
) { ) {
const cachedProfiles = findProfilesInCache(did)
for (let post of cachedProfiles) {
shadows.set(post, {...shadows.get(post), ...value})
}
batchedUpdates(() => { batchedUpdates(() => {
emitter.emit(uri, value) emitter.emit(did, value)
}) })
} }
function fromProfile(profile: ProfileView): ProfileShadow {
return {
followingUri: profile.viewer?.following,
muted: profile.viewer?.muted,
blockingUri: profile.viewer?.blocking,
}
}
function mergeShadow( function mergeShadow(
profile: ProfileView, profile: ProfileView,
shadow: ProfileShadow, shadow: Partial<ProfileShadow>,
): Shadow<ProfileView> { ): Shadow<ProfileView> {
return castAsShadow({ return castAsShadow({
...profile, ...profile,
viewer: { viewer: {
...(profile.viewer || {}), ...(profile.viewer || {}),
following: shadow.followingUri, following:
muted: shadow.muted, 'followingUri' in shadow
blocking: shadow.blockingUri, ? 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<ProfileView, void> {
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)
}

View file

@ -1,5 +1,10 @@
import {AppBskyGraphGetList} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
@ -31,3 +36,34 @@ export function useListMembersQuery(uri: string) {
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetList.OutputSchema>
>({
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
}
}
}
}
}
}

View file

@ -1,5 +1,10 @@
import {AppBskyGraphGetBlocks} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphGetBlocks} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -26,3 +31,26 @@ export function useMyBlockedAccountsQuery() {
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetBlocks.OutputSchema>
>({
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
}
}
}
}
}

View file

@ -1,5 +1,10 @@
import {AppBskyGraphGetMutes} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphGetMutes} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -26,3 +31,26 @@ export function useMyMutedAccountsQuery() {
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetMutes.OutputSchema>
>({
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
}
}
}
}
}

View file

@ -86,6 +86,19 @@ export function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): AppBskyFeedDefs.PostView | 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<AppBskyFeedDefs.PostView, void> {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({ const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: ['notification-feed'], queryKey: ['notification-feed'],
}) })
@ -96,10 +109,9 @@ export function findPostInQueryData(
for (const page of queryData?.pages) { for (const page of queryData?.pages) {
for (const item of page.items) { for (const item of page.items) {
if (item.subject?.uri === uri) { if (item.subject?.uri === uri) {
return item.subject yield item.subject
} }
} }
} }
} }
return undefined
} }

View file

@ -232,7 +232,20 @@ function createApi(
export function findPostInQueryData( export function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, 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<AppBskyFeedDefs.PostView, void> {
const queryDatas = queryClient.getQueriesData< const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected> InfiniteData<FeedPageUnselected>
>({ >({
@ -245,12 +258,23 @@ export function findPostInQueryData(
for (const page of queryData?.pages) { for (const page of queryData?.pages) {
for (const item of page.feed) { for (const item of page.feed) {
if (item.post.uri === uri) { 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[]) { function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {

View file

@ -1,5 +1,10 @@
import {AppBskyFeedGetLikes} from '@atproto/api' import {AppBskyActorDefs, AppBskyFeedGetLikes} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -31,3 +36,26 @@ export function usePostLikedByQuery(resolvedUri: string | undefined) {
enabled: !!resolvedUri, enabled: !!resolvedUri,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetLikes.OutputSchema>
>({
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
}
}
}
}
}

View file

@ -1,5 +1,10 @@
import {AppBskyFeedGetRepostedBy} from '@atproto/api' import {AppBskyActorDefs, AppBskyFeedGetRepostedBy} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -31,3 +36,26 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) {
enabled: !!resolvedUri, enabled: !!resolvedUri,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema>
>({
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
}
}
}
}
}

View file

@ -88,7 +88,7 @@ export function usePostThreadQuery(uri: string | undefined) {
{ {
const item = findPostInFeedQueryData(queryClient, uri) const item = findPostInFeedQueryData(queryClient, uri)
if (item) { if (item) {
return feedViewPostToPlaceholderThread(item) return postViewToPlaceholderThread(item)
} }
} }
{ {
@ -213,6 +213,19 @@ function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): ThreadNode | undefined { ): 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<ThreadNode, void> {
const queryDatas = queryClient.getQueriesData<ThreadNode>({ const queryDatas = queryClient.getQueriesData<ThreadNode>({
queryKey: ['post-thread'], queryKey: ['post-thread'],
}) })
@ -222,11 +235,10 @@ function findPostInQueryData(
} }
for (const item of traverseThread(queryData)) { for (const item of traverseThread(queryData)) {
if (item.uri === uri) { if (item.uri === uri) {
return item yield item
} }
} }
} }
return undefined
} }
function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> { function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {
@ -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( function postViewToPlaceholderThread(
post: AppBskyFeedDefs.PostView, post: AppBskyFeedDefs.PostView,
): ThreadNode { ): ThreadNode {

View file

@ -1,5 +1,10 @@
import {AppBskyGraphGetFollowers} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphGetFollowers} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -30,3 +35,26 @@ export function useProfileFollowersQuery(did: string | undefined) {
enabled: !!did, enabled: !!did,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetFollowers.OutputSchema>
>({
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
}
}
}
}
}

View file

@ -1,5 +1,10 @@
import {AppBskyGraphGetFollows} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphGetFollows} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
@ -33,3 +38,26 @@ export function useProfileFollowsQuery(did: string | undefined) {
enabled: !!did, enabled: !!did,
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetFollows.OutputSchema>
>({
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
}
}
}
}
}

View file

@ -5,7 +5,12 @@ import {
AppBskyActorProfile, AppBskyActorProfile,
AppBskyActorGetProfile, AppBskyActorGetProfile,
} from '@atproto/api' } 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 {Image as RNImage} from 'react-native-image-crop-picker'
import {useSession, getAgent} from '../session' import {useSession, getAgent} from '../session'
import {updateProfileShadow} from '../cache/profile-shadow' import {updateProfileShadow} from '../cache/profile-shadow'
@ -477,3 +482,21 @@ async function whenAppViewReady(
() => getAgent().app.bsky.actor.getProfile({actor}), () => getAgent().app.bsky.actor.getProfile({actor}),
) )
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewDetailed, void> {
const queryDatas =
queryClient.getQueriesData<AppBskyActorDefs.ProfileViewDetailed>({
queryKey: ['profile'],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
continue
}
if (queryData.did === did) {
yield queryData
}
}
}

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { import {
AppBskyActorDefs,
AppBskyActorGetSuggestions, AppBskyActorGetSuggestions,
AppBskyGraphGetSuggestedFollowsByActor, AppBskyGraphGetSuggestedFollowsByActor,
moderateProfile, moderateProfile,
@ -9,6 +10,7 @@ import {
useQueryClient, useQueryClient,
useQuery, useQuery,
InfiniteData, InfiniteData,
QueryClient,
QueryKey, QueryKey,
} from '@tanstack/react-query' } from '@tanstack/react-query'
@ -106,3 +108,56 @@ export function useGetSuggestedFollowersByActor() {
[queryClient], [queryClient],
) )
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
yield* findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did)
}
function* findAllProfilesInSuggestedFollowsQueryData(
queryClient: QueryClient,
did: string,
) {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyActorGetSuggestions.OutputSchema>
>({
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<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema>(
{
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
}
}
}
}