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

Configure Feed

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

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