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