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>zio/stable
parent
143fc80951
commit
46b63accb8
|
@ -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<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
|
||||
}
|
||||
const emitter = new EventEmitter()
|
||||
const shadows: WeakMap<
|
||||
AppBskyFeedDefs.PostView,
|
||||
Partial<PostShadow>
|
||||
> = new WeakMap()
|
||||
|
||||
export function usePostShadow(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
|
||||
const postSeenTS = getFirstSeenTS(post)
|
||||
const [state, setState] = useState<CacheEntry>(() => ({
|
||||
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<PostShadow>) => {
|
||||
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<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,
|
||||
}
|
||||
if (shadow) {
|
||||
return mergeShadow(post, shadow)
|
||||
} else {
|
||||
return castAsShadow(post)
|
||||
}
|
||||
}, [post, shadow])
|
||||
}
|
||||
|
||||
function mergeShadow(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
shadow: PostShadow,
|
||||
shadow: Partial<PostShadow>,
|
||||
): Shadow<AppBskyFeedDefs.PostView> | 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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ProfileView, number>()
|
||||
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<ProfileView, Partial<ProfileShadow>> = new WeakMap()
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
|
||||
const profileSeenTS = getFirstSeenTS(profile)
|
||||
const [state, setState] = useState<CacheEntry>(() => ({
|
||||
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<ProfileShadow>) => {
|
||||
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<ProfileShadow>,
|
||||
) {
|
||||
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<ProfileShadow>,
|
||||
): Shadow<ProfileView> {
|
||||
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<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)
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AppBskyFeedDefs.PostView, void> {
|
||||
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<AppBskyFeedDefs.PostView, void> {
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
InfiniteData<FeedPageUnselected>
|
||||
>({
|
||||
|
@ -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[]) {
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ThreadNode, void> {
|
||||
const queryDatas = queryClient.getQueriesData<ThreadNode>({
|
||||
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<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(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
): ThreadNode {
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue