Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 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}