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:
parent
143fc80951
commit
46b63accb8
14 changed files with 462 additions and 172 deletions
116
src/state/cache/profile-shadow.ts
vendored
116
src/state/cache/profile-shadow.ts
vendored
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue