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