Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

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