this repo has no description
1import {useEffect, useMemo, useState} from 'react'
2import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api'
3import {type QueryClient} from '@tanstack/react-query'
4import EventEmitter from 'eventemitter3'
5
6import {batchedUpdates} from '#/lib/batchedUpdates'
7import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions'
8import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '#/state/queries/actor-search'
9import {findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews'
10import {findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData} from '#/state/queries/find-contacts'
11import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
12import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members'
13import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations'
14import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
15import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts'
16import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed'
17import {
18 type FeedPage,
19 findAllProfilesInQueryData as findAllProfilesInFeedsQueryData,
20} from '#/state/queries/post-feed'
21import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
22import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
23import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
24import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile'
25import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
26import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
27import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
28import {findAllProfilesInQueryData as findAllProfilesInSuggestedOnboardingUsersQueryData} from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery'
29import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
30import {findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
31import type * as bsky from '#/types/bsky'
32import {castAsShadow, type Shadow} from './types'
33
34export type {Shadow} from './types'
35
36export interface ProfileShadow {
37 followingUri: string | undefined
38 muted: boolean | undefined
39 blockingUri: string | undefined
40 verification: AppBskyActorDefs.VerificationState
41 status: AppBskyActorDefs.StatusView | undefined
42 activitySubscription: AppBskyNotificationDefs.ActivitySubscription | undefined
43}
44
45const shadows: WeakMap<
46 bsky.profile.AnyProfileView,
47 Partial<ProfileShadow>
48> = new WeakMap()
49const emitter = new EventEmitter()
50
51export function useProfileShadow<
52 TProfileView extends bsky.profile.AnyProfileView,
53>(profile: TProfileView): Shadow<TProfileView> {
54 const [shadow, setShadow] = useState(() => shadows.get(profile))
55 const [prevPost, setPrevPost] = useState(profile)
56 if (profile !== prevPost) {
57 setPrevPost(profile)
58 setShadow(shadows.get(profile))
59 }
60
61 useEffect(() => {
62 function onUpdate() {
63 setShadow(shadows.get(profile))
64 }
65 emitter.addListener(profile.did, onUpdate)
66 return () => {
67 emitter.removeListener(profile.did, onUpdate)
68 }
69 }, [profile])
70
71 return useMemo(() => {
72 if (shadow) {
73 return mergeShadow(profile, shadow)
74 } else {
75 return castAsShadow(profile)
76 }
77 }, [profile, shadow])
78}
79
80/**
81 * Same as useProfileShadow, but allows for the profile to be undefined.
82 * This is useful for when the profile is not guaranteed to be loaded yet.
83 */
84export function useMaybeProfileShadow<
85 TProfileView extends bsky.profile.AnyProfileView,
86>(profile?: TProfileView): Shadow<TProfileView> | undefined {
87 const [shadow, setShadow] = useState(() =>
88 profile ? shadows.get(profile) : undefined,
89 )
90 const [prevPost, setPrevPost] = useState(profile)
91 if (profile !== prevPost) {
92 setPrevPost(profile)
93 setShadow(profile ? shadows.get(profile) : undefined)
94 }
95
96 useEffect(() => {
97 if (!profile) return
98 function onUpdate() {
99 if (!profile) return
100 setShadow(shadows.get(profile))
101 }
102 emitter.addListener(profile.did, onUpdate)
103 return () => {
104 emitter.removeListener(profile.did, onUpdate)
105 }
106 }, [profile])
107
108 return useMemo(() => {
109 if (!profile) return undefined
110 if (shadow) {
111 return mergeShadow(profile, shadow)
112 } else {
113 return castAsShadow(profile)
114 }
115 }, [profile, shadow])
116}
117
118/**
119 * Takes a list of posts, and returns a list of DIDs that should be filtered out
120 *
121 * Note: it doesn't retroactively scan the cache, but only listens to new updates.
122 * The use case here is intended for removing a post from a feed after you mute the author
123 */
124export function usePostAuthorShadowFilter(data?: FeedPage[]) {
125 const [trackedDids, setTrackedDids] = useState<string[]>(
126 () =>
127 data?.flatMap(page =>
128 page.slices.flatMap(slice =>
129 slice.items.map(item => item.post.author.did),
130 ),
131 ) ?? [],
132 )
133 const [authors, setAuthors] = useState(
134 new Map<string, {muted: boolean; blocked: boolean}>(),
135 )
136
137 const [prevData, setPrevData] = useState(data)
138 if (data !== prevData) {
139 const newAuthors = new Set(trackedDids)
140 let hasNew = false
141 for (const slice of data?.flatMap(page => page.slices) ?? []) {
142 for (const item of slice.items) {
143 const author = item.post.author
144 if (!newAuthors.has(author.did)) {
145 hasNew = true
146 newAuthors.add(author.did)
147 }
148 }
149 }
150 if (hasNew) setTrackedDids([...newAuthors])
151 setPrevData(data)
152 }
153
154 useEffect(() => {
155 const unsubs: Array<() => void> = []
156
157 for (const did of trackedDids) {
158 function onUpdate(value: Partial<ProfileShadow>) {
159 setAuthors(prev => {
160 const prevValue = prev.get(did)
161 const next = new Map(prev)
162 next.set(did, {
163 blocked: Boolean(value.blockingUri ?? prevValue?.blocked ?? false),
164 muted: Boolean(value.muted ?? prevValue?.muted ?? false),
165 })
166 return next
167 })
168 }
169 emitter.addListener(did, onUpdate)
170 unsubs.push(() => {
171 emitter.removeListener(did, onUpdate)
172 })
173 }
174
175 return () => {
176 unsubs.map(fn => fn())
177 }
178 }, [trackedDids])
179
180 return useMemo(() => {
181 const dids: Array<string> = []
182
183 for (const [did, value] of authors.entries()) {
184 if (value.blocked || value.muted) {
185 dids.push(did)
186 }
187 }
188
189 return dids
190 }, [authors])
191}
192
193export function updateProfileShadow(
194 queryClient: QueryClient,
195 did: string,
196 value: Partial<ProfileShadow>,
197) {
198 const cachedProfiles = findProfilesInCache(queryClient, did)
199 for (let profile of cachedProfiles) {
200 shadows.set(profile, {...shadows.get(profile), ...value})
201 }
202 batchedUpdates(() => {
203 emitter.emit(did, value)
204 })
205}
206
207function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
208 profile: TProfileView,
209 shadow: Partial<ProfileShadow>,
210): Shadow<TProfileView> {
211 return castAsShadow({
212 ...profile,
213 viewer: {
214 ...(profile.viewer || {}),
215 following:
216 'followingUri' in shadow
217 ? shadow.followingUri
218 : profile.viewer?.following,
219 muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
220 blocking:
221 'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
222 activitySubscription:
223 'activitySubscription' in shadow
224 ? shadow.activitySubscription
225 : profile.viewer?.activitySubscription,
226 },
227 verification:
228 'verification' in shadow ? shadow.verification : profile.verification,
229 status:
230 'status' in shadow
231 ? shadow.status
232 : 'status' in profile
233 ? profile.status
234 : undefined,
235 })
236}
237
238function* findProfilesInCache(
239 queryClient: QueryClient,
240 did: string,
241): Generator<bsky.profile.AnyProfileView, void> {
242 yield* findAllProfilesInListMembersQueryData(queryClient, did)
243 yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
244 yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
245 yield* findAllProfilesInPostLikedByQueryData(queryClient, did)
246 yield* findAllProfilesInPostRepostedByQueryData(queryClient, did)
247 yield* findAllProfilesInPostQuotesQueryData(queryClient, did)
248 yield* findAllProfilesInProfileQueryData(queryClient, did)
249 yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
250 yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
251 yield* findAllProfilesInSuggestedOnboardingUsersQueryData(queryClient, did)
252 yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
253 yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
254 yield* findAllProfilesInActorSearchQueryData(queryClient, did)
255 yield* findAllProfilesInListConvosQueryData(queryClient, did)
256 yield* findAllProfilesInFeedsQueryData(queryClient, did)
257 yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
258 yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
259 yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
260 yield* findAllProfilesInActivitySubscriptionsQueryData(queryClient, did)
261 yield* findAllProfilesInNotifsQueryData(queryClient, did)
262 yield* findAllProfilesInContactMatchesQueryData(queryClient, did)
263}