this repo has no description
0
fork

Configure Feed

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

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