264 lines
9.9 KiB
TypeScript
264 lines
9.9 KiB
TypeScript
import {useEffect, useMemo, useState} from 'react'
|
|
import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api'
|
|
import {type QueryClient} from '@tanstack/react-query'
|
|
import EventEmitter from 'eventemitter3'
|
|
|
|
import {batchedUpdates} from '#/lib/batchedUpdates'
|
|
import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions'
|
|
import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '#/state/queries/actor-search'
|
|
import {findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews'
|
|
import {findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData} from '#/state/queries/find-contacts'
|
|
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
|
|
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members'
|
|
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations'
|
|
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
|
|
import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts'
|
|
import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed'
|
|
import {
|
|
type FeedPage,
|
|
findAllProfilesInQueryData as findAllProfilesInFeedsQueryData,
|
|
} from '#/state/queries/post-feed'
|
|
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
|
|
import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
|
|
import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
|
|
import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile'
|
|
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
|
|
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
|
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
|
|
import {findAllProfilesInQueryData as findAllProfilesInSuggestedOnboardingUsersQueryData} from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery'
|
|
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
|
import {findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
|
import type * as bsky from '#/types/bsky'
|
|
import {castAsShadow, type Shadow} from './types'
|
|
|
|
export type {Shadow} from './types'
|
|
|
|
export interface ProfileShadow {
|
|
followingUri: string | undefined
|
|
muted: boolean | undefined
|
|
blockingUri: string | undefined
|
|
verification: AppBskyActorDefs.VerificationState
|
|
status: AppBskyActorDefs.StatusView | undefined
|
|
activitySubscription: AppBskyNotificationDefs.ActivitySubscription | undefined
|
|
}
|
|
|
|
const shadows: WeakMap<
|
|
bsky.profile.AnyProfileView,
|
|
Partial<ProfileShadow>
|
|
> = new WeakMap()
|
|
const emitter = new EventEmitter()
|
|
|
|
export function useProfileShadow<
|
|
TProfileView extends bsky.profile.AnyProfileView,
|
|
>(profile: TProfileView): Shadow<TProfileView> {
|
|
const [shadow, setShadow] = useState(() => shadows.get(profile))
|
|
const [prevPost, setPrevPost] = useState(profile)
|
|
if (profile !== prevPost) {
|
|
setPrevPost(profile)
|
|
setShadow(shadows.get(profile))
|
|
}
|
|
|
|
useEffect(() => {
|
|
function onUpdate() {
|
|
setShadow(shadows.get(profile))
|
|
}
|
|
emitter.addListener(profile.did, onUpdate)
|
|
return () => {
|
|
emitter.removeListener(profile.did, onUpdate)
|
|
}
|
|
}, [profile])
|
|
|
|
return useMemo(() => {
|
|
if (shadow) {
|
|
return mergeShadow(profile, shadow)
|
|
} else {
|
|
return castAsShadow(profile)
|
|
}
|
|
}, [profile, shadow])
|
|
}
|
|
|
|
/**
|
|
* Same as useProfileShadow, but allows for the profile to be undefined.
|
|
* This is useful for when the profile is not guaranteed to be loaded yet.
|
|
*/
|
|
export function useMaybeProfileShadow<
|
|
TProfileView extends bsky.profile.AnyProfileView,
|
|
>(profile?: TProfileView): Shadow<TProfileView> | undefined {
|
|
const [shadow, setShadow] = useState(() =>
|
|
profile ? shadows.get(profile) : undefined,
|
|
)
|
|
const [prevPost, setPrevPost] = useState(profile)
|
|
if (profile !== prevPost) {
|
|
setPrevPost(profile)
|
|
setShadow(profile ? shadows.get(profile) : undefined)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!profile) return
|
|
function onUpdate() {
|
|
if (!profile) return
|
|
setShadow(shadows.get(profile))
|
|
}
|
|
emitter.addListener(profile.did, onUpdate)
|
|
return () => {
|
|
emitter.removeListener(profile.did, onUpdate)
|
|
}
|
|
}, [profile])
|
|
|
|
return useMemo(() => {
|
|
if (!profile) return undefined
|
|
if (shadow) {
|
|
return mergeShadow(profile, shadow)
|
|
} else {
|
|
return castAsShadow(profile)
|
|
}
|
|
}, [profile, shadow])
|
|
}
|
|
|
|
/**
|
|
* Takes a list of posts, and returns a list of DIDs that should be filtered out
|
|
*
|
|
* Note: it doesn't retroactively scan the cache, but only listens to new updates.
|
|
* The use case here is intended for removing a post from a feed after you mute the author
|
|
*/
|
|
export function usePostAuthorShadowFilter(data?: FeedPage[]) {
|
|
const [trackedDids, setTrackedDids] = useState<string[]>(
|
|
() =>
|
|
data?.flatMap(page =>
|
|
page.slices.flatMap(slice =>
|
|
slice.items.map(item => item.post.author.did),
|
|
),
|
|
) ?? [],
|
|
)
|
|
const [authors, setAuthors] = useState(
|
|
new Map<string, {muted: boolean; blocked: boolean}>(),
|
|
)
|
|
|
|
useEffect(() => {
|
|
setTrackedDids(prev => {
|
|
const currentDids = new Set(prev)
|
|
let hasNew = false
|
|
for (const slice of data?.flatMap(page => page.slices) ?? []) {
|
|
for (const item of slice.items) {
|
|
const author = item.post.author
|
|
if (!currentDids.has(author.did)) {
|
|
hasNew = true
|
|
currentDids.add(author.did)
|
|
}
|
|
}
|
|
}
|
|
return hasNew ? [...currentDids] : prev
|
|
})
|
|
}, [data])
|
|
|
|
useEffect(() => {
|
|
const unsubs: Array<() => void> = []
|
|
|
|
for (const did of trackedDids) {
|
|
function onUpdate(value: Partial<ProfileShadow>) {
|
|
setAuthors(prev => {
|
|
const prevValue = prev.get(did)
|
|
const next = new Map(prev)
|
|
next.set(did, {
|
|
blocked: Boolean(value.blockingUri ?? prevValue?.blocked ?? false),
|
|
muted: Boolean(value.muted ?? prevValue?.muted ?? false),
|
|
})
|
|
return next
|
|
})
|
|
}
|
|
emitter.addListener(did, onUpdate)
|
|
unsubs.push(() => {
|
|
emitter.removeListener(did, onUpdate)
|
|
})
|
|
}
|
|
|
|
return () => {
|
|
unsubs.map(fn => fn())
|
|
}
|
|
}, [trackedDids])
|
|
|
|
return useMemo(() => {
|
|
const dids: Array<string> = []
|
|
|
|
for (const [did, value] of authors.entries()) {
|
|
if (value.blocked || value.muted) {
|
|
dids.push(did)
|
|
}
|
|
}
|
|
|
|
return dids
|
|
}, [authors])
|
|
}
|
|
|
|
export function updateProfileShadow(
|
|
queryClient: QueryClient,
|
|
did: string,
|
|
value: Partial<ProfileShadow>,
|
|
) {
|
|
const cachedProfiles = findProfilesInCache(queryClient, did)
|
|
for (let profile of cachedProfiles) {
|
|
shadows.set(profile, {...shadows.get(profile), ...value})
|
|
}
|
|
batchedUpdates(() => {
|
|
emitter.emit(did, value)
|
|
})
|
|
}
|
|
|
|
function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
|
|
profile: TProfileView,
|
|
shadow: Partial<ProfileShadow>,
|
|
): Shadow<TProfileView> {
|
|
return castAsShadow({
|
|
...profile,
|
|
viewer: {
|
|
...(profile.viewer || {}),
|
|
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,
|
|
activitySubscription:
|
|
'activitySubscription' in shadow
|
|
? shadow.activitySubscription
|
|
: profile.viewer?.activitySubscription,
|
|
},
|
|
verification:
|
|
'verification' in shadow ? shadow.verification : profile.verification,
|
|
status:
|
|
'status' in shadow
|
|
? shadow.status
|
|
: 'status' in profile
|
|
? profile.status
|
|
: undefined,
|
|
})
|
|
}
|
|
|
|
function* findProfilesInCache(
|
|
queryClient: QueryClient,
|
|
did: string,
|
|
): Generator<bsky.profile.AnyProfileView, void> {
|
|
yield* findAllProfilesInListMembersQueryData(queryClient, did)
|
|
yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
|
|
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
|
|
yield* findAllProfilesInPostLikedByQueryData(queryClient, did)
|
|
yield* findAllProfilesInPostRepostedByQueryData(queryClient, did)
|
|
yield* findAllProfilesInPostQuotesQueryData(queryClient, did)
|
|
yield* findAllProfilesInProfileQueryData(queryClient, did)
|
|
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
|
|
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
|
|
yield* findAllProfilesInSuggestedOnboardingUsersQueryData(queryClient, did)
|
|
yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
|
|
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
|
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
|
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
|
yield* findAllProfilesInFeedsQueryData(queryClient, did)
|
|
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
|
|
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
|
|
yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
|
|
yield* findAllProfilesInActivitySubscriptionsQueryData(queryClient, did)
|
|
yield* findAllProfilesInNotifsQueryData(queryClient, did)
|
|
yield* findAllProfilesInContactMatchesQueryData(queryClient, did)
|
|
}
|