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