diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index a566d00e8..11a960f7a 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -8,16 +8,22 @@ import { } from 'react' import {useWindowDimensions, View} from 'react-native' import Animated, {useAnimatedStyle} from 'react-native-reanimated' +import {moderatePost} from '@atproto/api' import {Trans} from '@lingui/react/macro' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' +import {usePostViewAuthorShadowFilter} from '#/state/cache/profile-shadow' import {useFeedFeedback} from '#/state/feed-feedback' import {useAlsoLikedCollapseByDefault} from '#/state/preferences/also-liked-collapse-by-default' import {useAlsoLikedFeedEnabled} from '#/state/preferences/also-liked-feed-enabled' -import {usePostAlsoLikedQuery} from '#/state/queries/post-also-liked' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import { + ALSO_LIKED_PAGE_SIZE, + usePostAlsoLikedQuery, +} from '#/state/queries/post-also-liked' import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' import { PostThreadContextProvider, @@ -66,6 +72,7 @@ export function PostThread({uri}: {uri: string}) { const {hasSession} = useSession() const initialNumToRender = useInitialNumToRender() const {height: windowHeight} = useWindowDimensions() + const moderationOpts = useModerationOpts() const anchorPostSource = useUnstablePostSource(uri) const feedFeedback = useFeedFeedback( anchorPostSource?.feedSourceInfo, @@ -159,9 +166,14 @@ export function PostThread({uri}: {uri: string}) { const [alsoLikedCollapsed, setAlsoLikedCollapsed] = useState( alsoLikedCollapseByDefault, ) + const [maxAlsoLikedCount, setMaxAlsoLikedCount] = + useState(ALSO_LIKED_PAGE_SIZE) useEffect(() => { setAlsoLikedCollapsed(alsoLikedCollapseByDefault) }, [alsoLikedAnchorUri, alsoLikedCollapseByDefault]) + useEffect(() => { + setMaxAlsoLikedCount(ALSO_LIKED_PAGE_SIZE) + }, [alsoLikedAnchorUri]) const alsoLikedVisible = Boolean(alsoLikedAnchorUri) && alsoLikedFeedEnabled && @@ -181,6 +193,22 @@ export function PostThread({uri}: {uri: string}) { return true }) }, [alsoLiked.data]) + const blockedOrMutedAlsoLikedAuthors = + usePostViewAuthorShadowFilter(alsoLikedPosts) + const filteredAlsoLikedPosts = useMemo(() => { + return alsoLikedPosts.filter(post => { + if (blockedOrMutedAlsoLikedAuthors.includes(post.author.did)) { + return false + } + if (!moderationOpts) { + return true + } + return !moderatePost(post, moderationOpts).ui('contentList').filter + }) + }, [alsoLikedPosts, blockedOrMutedAlsoLikedAuthors, moderationOpts]) + const visibleAlsoLikedPosts = useMemo(() => { + return filteredAlsoLikedPosts.slice(0, maxAlsoLikedCount) + }, [filteredAlsoLikedPosts, maxAlsoLikedCount]) const [maxParentCount, setMaxParentCount] = useState(PARENT_CHUNK_SIZE) const [maxChildrenCount, setMaxChildrenCount] = useState(CHILDREN_CHUNK_SIZE) const totalParentCount = useRef(0) // recomputed below @@ -201,6 +229,12 @@ export function PostThread({uri}: {uri: string}) { } }, [alsoLikedCollapsed, alsoLikedVisible]) + useEffect(() => { + if (alsoLikedCollapsed) { + setMaxAlsoLikedCount(ALSO_LIKED_PAGE_SIZE) + } + }, [alsoLikedCollapsed]) + /* * On a cold load, parents are not prepended until the anchor post has * rendered as the first item in the list. This gives us a consistent @@ -243,8 +277,8 @@ export function PostThread({uri}: {uri: string}) { contentSizeAnimationFrameRef.current = requestAnimationFrame(() => { contentSizeAnimationFrameRef.current = null const list = listRef.current - const anchorElement = anchorRef.current as any as Element - const header = headerRef.current as any as Element + const anchorElement = anchorRef.current as unknown as Element | null + const header = headerRef.current as unknown as Element | null if (list && anchorElement && header && shouldHandleScroll.current) { const anchorOffsetTop = anchorElement.getBoundingClientRect().top @@ -388,14 +422,24 @@ export function PostThread({uri}: {uri: string}) { setMaxChildrenCount(prev => prev + CHILDREN_CHUNK_SIZE) return } - if ( - alsoLikedAnchorUri && - alsoLikedEnabled && - !alsoLiked.isLoading && - !alsoLiked.isFetchingNextPage && - alsoLiked.hasNextPage - ) { - void alsoLiked.fetchNextPage() + if (alsoLikedAnchorUri && alsoLikedEnabled) { + if (visibleAlsoLikedPosts.length < filteredAlsoLikedPosts.length) { + setMaxAlsoLikedCount(current => + Math.min( + current + ALSO_LIKED_PAGE_SIZE, + filteredAlsoLikedPosts.length, + ), + ) + return + } + + if ( + !alsoLiked.isLoading && + !alsoLiked.isFetchingNextPage && + alsoLiked.hasNextPage + ) { + void alsoLiked.fetchNextPage() + } } } @@ -739,7 +783,7 @@ export function PostThread({uri}: {uri: string}) { })} onStartReached={onStartReached} onEndReached={onEndReached} - onEndReachedThreshold={4} + onEndReachedThreshold={2} onStartReachedThreshold={1} onScrollOffsetChange={handleScrollOffsetChange} onItemSeen={item => { @@ -757,15 +801,17 @@ export function PostThread({uri}: {uri: string}) { sideBorders={false} ListFooterComponent={ void, ) { - const target = view as any + const target = view as + | (View & { + measureInWindow?: ( + callback: ( + x: number, + y: number, + width: number, + height: number, + ) => void, + ) => void + getBoundingClientRect?: () => DOMRect + measure?: ( + callback: ( + x: number, + y: number, + width: number, + height: number, + pageX: number, + pageY: number, + ) => void, + ) => void + }) + | null if (!target) { cb(null) return diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 71dce5fe8..a9f4c937c 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -1,5 +1,9 @@ import {useEffect, useMemo, useState} from 'react' -import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api' +import { + type AppBskyActorDefs, + type AppBskyFeedDefs, + type AppBskyNotificationDefs, +} from '@atproto/api' import {type QueryClient} from '@tanstack/react-query' import {EventEmitter} from 'eventemitter3' @@ -195,6 +199,69 @@ export function usePostAuthorShadowFilter(data?: FeedPage[]) { }, [authors]) } +export function usePostViewAuthorShadowFilter( + posts?: AppBskyFeedDefs.PostView[], +) { + const [trackedDids, setTrackedDids] = useState( + () => posts?.map(post => post.author.did) ?? [], + ) + const [authors, setAuthors] = useState( + new Map(), + ) + + useEffect(() => { + setTrackedDids(prev => { + const currentDids = new Set(prev) + let hasNew = false + for (const post of posts ?? []) { + if (!currentDids.has(post.author.did)) { + hasNew = true + currentDids.add(post.author.did) + } + } + return hasNew ? [...currentDids] : prev + }) + }, [posts]) + + useEffect(() => { + const unsubs: Array<() => void> = [] + + for (const did of trackedDids) { + function onUpdate(value: Partial) { + setAuthors(prev => { + const prevValue = prev.get(did) + const next = new Map(prev) + next.set(did, { + blocked: Boolean(value.blockingUri ?? prevValue?.blocked ?? false), + muted: Boolean(value.muted ?? prevValue?.muted ?? false), + }) + return next + }) + } + emitter.addListener(did, onUpdate) + unsubs.push(() => { + emitter.removeListener(did, onUpdate) + }) + } + + return () => { + unsubs.map(fn => fn()) + } + }, [trackedDids]) + + return useMemo(() => { + const dids: Array = [] + + for (const [did, value] of authors.entries()) { + if (value.blocked || value.muted) { + dids.push(did) + } + } + + return dids + }, [authors]) +} + export function updateProfileShadow( queryClient: QueryClient, did: string, diff --git a/src/state/queries/post-also-liked.ts b/src/state/queries/post-also-liked.ts index 089045721..fc995400b 100644 --- a/src/state/queries/post-also-liked.ts +++ b/src/state/queries/post-also-liked.ts @@ -12,7 +12,7 @@ import {useAgent} from '#/state/session' import {embedViewRecordToPostView, getEmbeddedPost} from './util' const ALSO_LIKED_URL = 'https://foryou.club/also-liked' -const ALSO_LIKED_PAGE_SIZE = 10 +export const ALSO_LIKED_PAGE_SIZE = 10 type AlsoLikedSkeletonResponse = { cursor?: string