···13import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
14import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts'
15import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed'
16-import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '#/state/queries/post-feed'
00017import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
18import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
19import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
···108 return castAsShadow(profile)
109 }
110 }, [profile, shadow])
000000000000000000000000000000000000000000000000000000000000000000000000000111}
112113export function updateProfileShadow(
···13import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
14import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts'
15import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed'
16+import {
17+ type FeedPage,
18+ findAllProfilesInQueryData as findAllProfilesInFeedsQueryData,
19+} from '#/state/queries/post-feed'
20import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
21import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
22import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
···111 return castAsShadow(profile)
112 }
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])
189}
190191export function updateProfileShadow(
+18-1
src/view/com/posts/PostFeed.tsx
···35import {isNetworkError} from '#/lib/strings/errors'
36import {logger} from '#/logger'
37import {isIOS, isNative, isWeb} from '#/platform/detection'
038import {listenPostCreated} from '#/state/events'
39import {useFeedFeedbackContext} from '#/state/feed-feedback'
40import {useTrendingSettings} from '#/state/preferences/trending'
···363 */
364 const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed)
36500000366 const feedItems: FeedRow[] = useMemo(() => {
367 // wraps a slice item, and replaces it with a showLessFollowup item
368 // if the user has pressed show less on it
···423 // eslint-disable-next-line @typescript-eslint/no-shadow
424 item => item.uri === slice.feedPostUri,
425 )
426- if (item && AppBskyEmbedVideo.isView(item.post.embed)) {
0000427 videos.push({
428 item,
429 feedContext: slice.feedContext,
···541 key:
542 'sliceFallbackMarker-' + sliceIndex + '-' + lastFetchedAt,
543 })
000000544 } else if (slice.isIncompleteThread && slice.items.length >= 3) {
545 const beforeLast = slice.items.length - 2
546 const last = slice.items.length - 1
···636 hasPressedShowLessUris,
637 ageAssuranceBannerState,
638 isCurrentFeedAtStartupSelected,
0639 ])
640641 // events
···35import {isNetworkError} from '#/lib/strings/errors'
36import {logger} from '#/logger'
37import {isIOS, isNative, isWeb} from '#/platform/detection'
38+import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
39import {listenPostCreated} from '#/state/events'
40import {useFeedFeedbackContext} from '#/state/feed-feedback'
41import {useTrendingSettings} from '#/state/preferences/trending'
···364 */
365 const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed)
366367+ const blockedOrMutedAuthors = usePostAuthorShadowFilter(
368+ // author feeds have their own handling
369+ feed.startsWith('author|') ? undefined : data?.pages,
370+ )
371+372 const feedItems: FeedRow[] = useMemo(() => {
373 // wraps a slice item, and replaces it with a showLessFollowup item
374 // if the user has pressed show less on it
···429 // eslint-disable-next-line @typescript-eslint/no-shadow
430 item => item.uri === slice.feedPostUri,
431 )
432+ if (
433+ item &&
434+ AppBskyEmbedVideo.isView(item.post.embed) &&
435+ !blockedOrMutedAuthors.includes(item.post.author.did)
436+ ) {
437 videos.push({
438 item,
439 feedContext: slice.feedContext,
···551 key:
552 'sliceFallbackMarker-' + sliceIndex + '-' + lastFetchedAt,
553 })
554+ } else if (
555+ slice.items.some(item =>
556+ blockedOrMutedAuthors.includes(item.post.author.did),
557+ )
558+ ) {
559+ // skip
560 } else if (slice.isIncompleteThread && slice.items.length >= 3) {
561 const beforeLast = slice.items.length - 2
562 const last = slice.items.length - 1
···652 hasPressedShowLessUris,
653 ageAssuranceBannerState,
654 isCurrentFeedAtStartupSelected,
655+ blockedOrMutedAuthors,
656 ])
657658 // events