Bluesky app fork with some witchin' additions 💫

add shadow filter to post feeds (#9406)

authored by samuel.fm and committed by GitHub 55eb6c56 051dbd28

Changed files
+97 -2
src
state
view
com
posts
+79 -1
src/state/cache/profile-shadow.ts
··· 13 13 import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts' 14 14 import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts' 15 15 import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed' 16 - import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '#/state/queries/post-feed' 16 + import { 17 + type FeedPage, 18 + findAllProfilesInQueryData as findAllProfilesInFeedsQueryData, 19 + } from '#/state/queries/post-feed' 17 20 import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' 18 21 import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' 19 22 import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' ··· 108 111 return castAsShadow(profile) 109 112 } 110 113 }, [profile, shadow]) 114 + } 115 + 116 + /** 117 + * Takes a list of posts, and returns a list of DIDs that should be filtered out 118 + * 119 + * Note: it doesn't retroactively scan the cache, but only listens to new updates. 120 + * The use case here is intended for removing a post from a feed after you mute the author 121 + */ 122 + export function usePostAuthorShadowFilter(data?: FeedPage[]) { 123 + const [trackedDids, setTrackedDids] = useState<string[]>( 124 + () => 125 + data?.flatMap(page => 126 + page.slices.flatMap(slice => 127 + slice.items.map(item => item.post.author.did), 128 + ), 129 + ) ?? [], 130 + ) 131 + const [authors, setAuthors] = useState( 132 + new Map<string, {muted: boolean; blocked: boolean}>(), 133 + ) 134 + 135 + const [prevData, setPrevData] = useState(data) 136 + if (data !== prevData) { 137 + const newAuthors = new Set(trackedDids) 138 + let hasNew = false 139 + for (const slice of data?.flatMap(page => page.slices) ?? []) { 140 + for (const item of slice.items) { 141 + const author = item.post.author 142 + if (!newAuthors.has(author.did)) { 143 + hasNew = true 144 + newAuthors.add(author.did) 145 + } 146 + } 147 + } 148 + if (hasNew) setTrackedDids([...newAuthors]) 149 + setPrevData(data) 150 + } 151 + 152 + useEffect(() => { 153 + const unsubs: Array<() => void> = [] 154 + 155 + for (const did of trackedDids) { 156 + function onUpdate(value: Partial<ProfileShadow>) { 157 + setAuthors(prev => { 158 + const prevValue = prev.get(did) 159 + const next = new Map(prev) 160 + next.set(did, { 161 + blocked: Boolean(value.blockingUri ?? prevValue?.blocked ?? false), 162 + muted: Boolean(value.muted ?? prevValue?.muted ?? false), 163 + }) 164 + return next 165 + }) 166 + } 167 + emitter.addListener(did, onUpdate) 168 + unsubs.push(() => { 169 + emitter.removeListener(did, onUpdate) 170 + }) 171 + } 172 + 173 + return () => { 174 + unsubs.map(fn => fn()) 175 + } 176 + }, [trackedDids]) 177 + 178 + return useMemo(() => { 179 + const dids: Array<string> = [] 180 + 181 + for (const [did, value] of authors.entries()) { 182 + if (value.blocked || value.muted) { 183 + dids.push(did) 184 + } 185 + } 186 + 187 + return dids 188 + }, [authors]) 111 189 } 112 190 113 191 export function updateProfileShadow(
+18 -1
src/view/com/posts/PostFeed.tsx
··· 35 35 import {isNetworkError} from '#/lib/strings/errors' 36 36 import {logger} from '#/logger' 37 37 import {isIOS, isNative, isWeb} from '#/platform/detection' 38 + import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow' 38 39 import {listenPostCreated} from '#/state/events' 39 40 import {useFeedFeedbackContext} from '#/state/feed-feedback' 40 41 import {useTrendingSettings} from '#/state/preferences/trending' ··· 363 364 */ 364 365 const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed) 365 366 367 + const blockedOrMutedAuthors = usePostAuthorShadowFilter( 368 + // author feeds have their own handling 369 + feed.startsWith('author|') ? undefined : data?.pages, 370 + ) 371 + 366 372 const feedItems: FeedRow[] = useMemo(() => { 367 373 // wraps a slice item, and replaces it with a showLessFollowup item 368 374 // if the user has pressed show less on it ··· 423 429 // eslint-disable-next-line @typescript-eslint/no-shadow 424 430 item => item.uri === slice.feedPostUri, 425 431 ) 426 - if (item && AppBskyEmbedVideo.isView(item.post.embed)) { 432 + if ( 433 + item && 434 + AppBskyEmbedVideo.isView(item.post.embed) && 435 + !blockedOrMutedAuthors.includes(item.post.author.did) 436 + ) { 427 437 videos.push({ 428 438 item, 429 439 feedContext: slice.feedContext, ··· 541 551 key: 542 552 'sliceFallbackMarker-' + sliceIndex + '-' + lastFetchedAt, 543 553 }) 554 + } else if ( 555 + slice.items.some(item => 556 + blockedOrMutedAuthors.includes(item.post.author.did), 557 + ) 558 + ) { 559 + // skip 544 560 } else if (slice.isIncompleteThread && slice.items.length >= 3) { 545 561 const beforeLast = slice.items.length - 2 546 562 const last = slice.items.length - 1 ··· 636 652 hasPressedShowLessUris, 637 653 ageAssuranceBannerState, 638 654 isCurrentFeedAtStartupSelected, 655 + blockedOrMutedAuthors, 639 656 ]) 640 657 641 658 // events