diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx index 352cc1dc0..a1bae1b05 100644 --- a/src/view/com/posts/FollowingEmptyState.tsx +++ b/src/view/com/posts/FollowingEmptyState.tsx @@ -1,19 +1,19 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' +import { StyleSheet, View } from 'react-native' import { FontAwesomeIcon, type FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {Trans} from '@lingui/macro' -import {useNavigation} from '@react-navigation/native' +import { Trans } from '@lingui/macro' +import { useNavigation } from '@react-navigation/native' -import {usePalette} from '#/lib/hooks/usePalette' -import {MagnifyingGlassIcon} from '#/lib/icons' -import {type NavigationProp} from '#/lib/routes/types' -import {s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' +import { usePalette } from '#/lib/hooks/usePalette' +import { MagnifyingGlassIcon } from '#/lib/icons' +import { type NavigationProp } from '#/lib/routes/types' +import { s } from '#/lib/styles' +import { isWeb } from '#/platform/detection' +import { Button } from '../util/forms/Button' +import { Text } from '../util/text/Text' export function FollowingEmptyState() { const pal = usePalette('default') @@ -45,36 +45,6 @@ export function FollowingEmptyState() { happening. - - - - You can also discover new Custom Feeds to follow. - - ) diff --git a/src/view/com/posts/FollowingEndOfFeed.tsx b/src/view/com/posts/FollowingEndOfFeed.tsx index e3c84d782..a7b210db9 100644 --- a/src/view/com/posts/FollowingEndOfFeed.tsx +++ b/src/view/com/posts/FollowingEndOfFeed.tsx @@ -1,18 +1,18 @@ import React from 'react' -import {Dimensions, StyleSheet, View} from 'react-native' +import { Dimensions, StyleSheet, View } from 'react-native' import { FontAwesomeIcon, type FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {Trans} from '@lingui/macro' -import {useNavigation} from '@react-navigation/native' +import { Trans } from '@lingui/macro' +import { useNavigation } from '@react-navigation/native' -import {usePalette} from '#/lib/hooks/usePalette' -import {type NavigationProp} from '#/lib/routes/types' -import {s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' +import { usePalette } from '#/lib/hooks/usePalette' +import { type NavigationProp } from '#/lib/routes/types' +import { s } from '#/lib/styles' +import { isWeb } from '#/platform/detection' +import { Button } from '../util/forms/Button' +import { Text } from '../util/text/Text' export function FollowingEndOfFeed() { const pal = usePalette('default') @@ -37,7 +37,7 @@ export function FollowingEndOfFeed() { style={[ styles.container, pal.border, - {minHeight: Dimensions.get('window').height * 0.75}, + { minHeight: Dimensions.get('window').height * 0.75 }, ]}> @@ -46,36 +46,6 @@ export function FollowingEndOfFeed() { follow. - - - - You can also discover new Custom Feeds to follow. - - ) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 4f25468c9..b9f2db999 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -23,23 +23,23 @@ import { AppBskyEmbedVideo, type AppBskyFeedDefs, } from '@atproto/api' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useQueryClient} from '@tanstack/react-query' +import { msg } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { useQueryClient } from '@tanstack/react-query' -import {isStatusStillActive, validateStatus} from '#/lib/actor-status' -import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' -import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' -import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {logEvent, useGate} from '#/lib/statsig/statsig' -import {isNetworkError} from '#/lib/strings/errors' -import {logger} from '#/logger' -import {isIOS, isNative, isWeb} from '#/platform/detection' -import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow' -import {listenPostCreated} from '#/state/events' -import {useFeedFeedbackContext} from '#/state/feed-feedback' -import {useTrendingSettings} from '#/state/preferences/trending' -import {STALE} from '#/state/queries' +import { isStatusStillActive, validateStatus } from '#/lib/actor-status' +import { DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS } from '#/lib/constants' +import { useInitialNumToRender } from '#/lib/hooks/useInitialNumToRender' +import { useNonReactiveCallback } from '#/lib/hooks/useNonReactiveCallback' +import { logEvent, useGate } from '#/lib/statsig/statsig' +import { isNetworkError } from '#/lib/strings/errors' +import { logger } from '#/logger' +import { isIOS, isNative, isWeb } from '#/platform/detection' +import { usePostAuthorShadowFilter } from '#/state/cache/profile-shadow' +import { listenPostCreated } from '#/state/events' +import { useFeedFeedbackContext } from '#/state/feed-feedback' +import { useTrendingSettings } from '#/state/preferences/trending' +import { STALE } from '#/state/queries' import { type AuthorFilter, type FeedDescriptor, @@ -50,111 +50,111 @@ import { RQKEY, usePostFeedQuery, } from '#/state/queries/post-feed' -import {useLiveNowConfig} from '#/state/service-config' -import {useSession} from '#/state/session' -import {useProgressGuide} from '#/state/shell/progress-guide' -import {useSelectedFeed} from '#/state/shell/selected-feed' -import {List, type ListRef} from '#/view/com/util/List' -import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' -import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' -import {useBreakpoints, useLayoutBreakpoints} from '#/alf' +import { useLiveNowConfig } from '#/state/service-config' +import { useSession } from '#/state/session' +import { useProgressGuide } from '#/state/shell/progress-guide' +import { useSelectedFeed } from '#/state/shell/selected-feed' +import { List, type ListRef } from '#/view/com/util/List' +import { PostFeedLoadingPlaceholder } from '#/view/com/util/LoadingPlaceholder' +import { LoadMoreRetryBtn } from '#/view/com/util/LoadMoreRetryBtn' +import { type VideoFeedSourceContext } from '#/screens/VideoFeed/types' +import { useBreakpoints, useLayoutBreakpoints } from '#/alf' import { AgeAssuranceDismissibleFeedBanner, useInternalState as useAgeAssuranceBannerState, } from '#/components/ageAssurance/AgeAssuranceDismissibleFeedBanner' -import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' +import { ProgressGuide, SuggestedFollows } from '#/components/FeedInterstitials' import { PostFeedVideoGridRow, PostFeedVideoGridRowPlaceholder, } from '#/components/feeds/PostFeedVideoGridRow' -import {TrendingInterstitial} from '#/components/interstitials/Trending' -import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' -import {ComposerPrompt} from '../feeds/ComposerPrompt' -import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' -import {FeedShutdownMsg} from './FeedShutdownMsg' -import {PostFeedErrorMessage} from './PostFeedErrorMessage' -import {PostFeedItem} from './PostFeedItem' -import {ShowLessFollowup} from './ShowLessFollowup' -import {ViewFullThread} from './ViewFullThread' +import { TrendingInterstitial } from '#/components/interstitials/Trending' +import { TrendingVideos as TrendingVideosInterstitial } from '#/components/interstitials/TrendingVideos' +import { ComposerPrompt } from '../feeds/ComposerPrompt' +import { DiscoverFallbackHeader } from './DiscoverFallbackHeader' +import { FeedShutdownMsg } from './FeedShutdownMsg' +import { PostFeedErrorMessage } from './PostFeedErrorMessage' +import { PostFeedItem } from './PostFeedItem' +import { ShowLessFollowup } from './ShowLessFollowup' +import { ViewFullThread } from './ViewFullThread' type FeedRow = | { - type: 'loading' - key: string - } + type: 'loading' + key: string + } | { - type: 'empty' - key: string - } + type: 'empty' + key: string + } | { - type: 'error' - key: string - } + type: 'error' + key: string + } | { - type: 'loadMoreError' - key: string - } + type: 'loadMoreError' + key: string + } | { - type: 'feedShutdownMsg' - key: string - } + type: 'feedShutdownMsg' + key: string + } | { - type: 'fallbackMarker' - key: string - } + type: 'fallbackMarker' + key: string + } | { - type: 'sliceItem' - key: string - slice: FeedPostSlice - indexInSlice: number - showReplyTo: boolean - } + type: 'sliceItem' + key: string + slice: FeedPostSlice + indexInSlice: number + showReplyTo: boolean + } | { - type: 'videoGridRowPlaceholder' - key: string - } + type: 'videoGridRowPlaceholder' + key: string + } | { - type: 'videoGridRow' - key: string - items: FeedPostSliceItem[] - sourceFeedUri: string - feedContexts: (string | undefined)[] - reqIds: (string | undefined)[] - } + type: 'videoGridRow' + key: string + items: FeedPostSliceItem[] + sourceFeedUri: string + feedContexts: (string | undefined)[] + reqIds: (string | undefined)[] + } | { - type: 'sliceViewFullThread' - key: string - uri: string - } + type: 'sliceViewFullThread' + key: string + uri: string + } | { - type: 'interstitialFollows' - key: string - } + type: 'interstitialFollows' + key: string + } | { - type: 'interstitialProgressGuide' - key: string - } + type: 'interstitialProgressGuide' + key: string + } | { - type: 'interstitialTrending' - key: string - } + type: 'interstitialTrending' + key: string + } | { - type: 'interstitialTrendingVideos' - key: string - } + type: 'interstitialTrendingVideos' + key: string + } | { - type: 'showLessFollowup' - key: string - } + type: 'showLessFollowup' + key: string + } | { - type: 'ageAssuranceBanner' - key: string - } + type: 'ageAssuranceBanner' + key: string + } | { - type: 'composerPrompt' - key: string - } + type: 'composerPrompt' + key: string + } export function getItemsForFeedback(feedRow: FeedRow): { item: FeedPostSliceItem @@ -227,17 +227,17 @@ let PostFeed = ({ initialNumToRender?: number isVideoFeed?: boolean }): React.ReactNode => { - const {_} = useLingui() + const { _ } = useLingui() const queryClient = useQueryClient() - const {currentAccount, hasSession} = useSession() + const { currentAccount, hasSession } = useSession() const gate = useGate() const initialNumToRender = useInitialNumToRender() const feedFeedback = useFeedFeedbackContext() const [isPTRing, setIsPTRing] = useState(false) const lastFetchRef = useRef(Date.now()) const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') - const {gtMobile} = useBreakpoints() - const {rightNavVisible} = useLayoutBreakpoints() + const { gtMobile } = useBreakpoints() + const { rightNavVisible } = useLayoutBreakpoints() const areVideoFeedsEnabled = isNative const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState( @@ -256,7 +256,7 @@ let PostFeed = ({ const feedCacheKey = feedParams?.feedCacheKey const opts = useMemo( - () => ({enabled, ignoreFilterFor}), + () => ({ enabled, ignoreFilterFor }), [enabled, ignoreFilterFor], ) const { @@ -299,7 +299,7 @@ let PostFeed = ({ } } catch (e) { if (!isNetworkError(e)) { - logger.error('Poll latest failed', {feed, message: String(e)}) + logger.error('Poll latest failed', { feed, message: String(e) }) } } }) @@ -315,7 +315,7 @@ let PostFeed = ({ (feed === 'following' || feed === `author|${myDid}|posts_and_author_threads`) ) { - queryClient.invalidateQueries({queryKey: RQKEY(feed)}) + queryClient.invalidateQueries({ queryKey: RQKEY(feed) }) } }, [queryClient, feed, data, myDid]) useEffect(() => { @@ -360,7 +360,7 @@ let PostFeed = ({ const showProgressIntersitial = (followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible - const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() + const { trendingDisabled, trendingVideoDisabled } = useTrendingSettings() const ageAssuranceBannerState = useAgeAssuranceBannerState() const selectedFeed = useSelectedFeed() @@ -378,7 +378,7 @@ let PostFeed = ({ const feedItems: FeedRow[] = useMemo(() => { // wraps a slice item, and replaces it with a showLessFollowup item // if the user has pressed show less on it - const sliceItem = (row: Extract) => { + const sliceItem = (row: Extract) => { if (hasPressedShowLessUris.has(row.slice.items[row.indexInSlice]?.uri)) { return { type: 'showLessFollowup', @@ -537,403 +537,437 @@ let PostFeed = ({ }) } } else if (sliceIndex === 30) { - arr.push({ - type: 'interstitialFollows', - key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, - }) } } else if (feedKind === 'following') { if (sliceIndex === 0) { - // Show composer prompt for Following feed - if (hasSession && gate('show_composer_prompt')) { - arr.push({ - type: 'composerPrompt', - key: 'composerPrompt-' + sliceIndex, - }) - } + }) +} } } else if (feedKind === 'profile') { - if (sliceIndex === 5) { - arr.push({ - type: 'interstitialFollows', - key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, - }) - } - } else { - /* - * Only insert if this feed was the last selected feed at - * startup and the banner is eligible to be shown. - */ - if (sliceIndex === 0 && isCurrentFeedAtStartupSelected) { - arr.push({ - type: 'ageAssuranceBanner', - key: 'ageAssuranceBanner-' + sliceIndex, - }) - } - } + if (sliceIndex === 5) { + arr.push({ + type: 'interstitialFollows', + key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, + }) + } +} else { + /* + * Only insert if this feed was the last selected feed at + * startup and the banner is eligible to be shown. + */ + if (sliceIndex === 0 && isCurrentFeedAtStartupSelected) { + arr.push({ + type: 'ageAssuranceBanner', + key: 'ageAssuranceBanner-' + sliceIndex, + }) + } +} } - if (slice.isFallbackMarker) { - arr.push({ - type: 'fallbackMarker', - key: - 'sliceFallbackMarker-' + sliceIndex + '-' + lastFetchedAt, - }) - } else if ( - slice.items.some(item => - blockedOrMutedAuthors.includes(item.post.author.did), - ) - ) { - // skip - } else if (slice.isIncompleteThread && slice.items.length >= 3) { - const beforeLast = slice.items.length - 2 - const last = slice.items.length - 1 - arr.push( - sliceItem({ - type: 'sliceItem', - key: slice.items[0]._reactKey, - slice: slice, - indexInSlice: 0, - showReplyTo: false, - }), - ) - arr.push({ - type: 'sliceViewFullThread', - key: slice._reactKey + '-viewFullThread', - uri: slice.items[0].uri, - }) - arr.push( - sliceItem({ - type: 'sliceItem', - key: slice.items[beforeLast]._reactKey, - slice: slice, - indexInSlice: beforeLast, - showReplyTo: - slice.items[beforeLast].parentAuthor?.did !== - slice.items[beforeLast].post.author.did, - }), - ) - arr.push( - sliceItem({ - type: 'sliceItem', - key: slice.items[last]._reactKey, - slice: slice, - indexInSlice: last, - showReplyTo: false, - }), - ) - } else { - for (let i = 0; i < slice.items.length; i++) { - arr.push( - sliceItem({ - type: 'sliceItem', - key: slice.items[i]._reactKey, - slice: slice, - indexInSlice: i, - showReplyTo: i === 0, - }), - ) - } - } +if (slice.isFallbackMarker) { + arr.push({ + type: 'fallbackMarker', + key: + 'sliceFallbackMarker-' + sliceIndex + '-' + lastFetchedAt, + }) +} else if ( + slice.items.some(item => + blockedOrMutedAuthors.includes(item.post.author.did), + ) +) { + // skip +} else if (slice.isIncompleteThread && slice.items.length >= 3) { + const beforeLast = slice.items.length - 2 + const last = slice.items.length - 1 + arr.push( + sliceItem({ + type: 'sliceItem', + key: slice.items[0]._reactKey, + slice: slice, + indexInSlice: 0, + showReplyTo: false, + }), + ) + arr.push({ + type: 'sliceViewFullThread', + key: slice._reactKey + '-viewFullThread', + uri: slice.items[0].uri, + }) + arr.push( + sliceItem({ + type: 'sliceItem', + key: slice.items[beforeLast]._reactKey, + slice: slice, + indexInSlice: beforeLast, + showReplyTo: + slice.items[beforeLast].parentAuthor?.did !== + slice.items[beforeLast].post.author.did, + }), + ) + arr.push( + sliceItem({ + type: 'sliceItem', + key: slice.items[last]._reactKey, + slice: slice, + indexInSlice: last, + showReplyTo: false, + }), + ) +} else { + for (let i = 0; i < slice.items.length; i++) { + arr.push( + sliceItem({ + type: 'sliceItem', + key: slice.items[i]._reactKey, + slice: slice, + indexInSlice: i, + showReplyTo: i === 0, + }), + ) + } +} } } } } - if (isError && !isEmpty) { - arr.push({ - type: 'loadMoreError', - key: 'loadMoreError', - }) - } +if (isError && !isEmpty) { + arr.push({ + type: 'loadMoreError', + key: 'loadMoreError', + }) +} } else { - if (isVideoFeed) { - arr.push({ - type: 'videoGridRowPlaceholder', - key: 'videoGridRowPlaceholder', - }) - } else { - arr.push({ - type: 'loading', - key: 'loading', - }) - } - } + if (isVideoFeed) { + arr.push({ + type: 'videoGridRowPlaceholder', + key: 'videoGridRowPlaceholder', + }) + } else { + arr.push({ + type: 'loading', + key: 'loading', + }) + } +} - return arr +return arr }, [ - isFetched, - isError, - isEmpty, - lastFetchedAt, - data, - feed, - feedType, - feedUriOrActorDid, - feedTab, - hasSession, - showProgressIntersitial, - trendingDisabled, - trendingVideoDisabled, - rightNavVisible, - gtMobile, - isVideoFeed, - areVideoFeedsEnabled, - hasPressedShowLessUris, - ageAssuranceBannerState, - isCurrentFeedAtStartupSelected, - gate, - blockedOrMutedAuthors, - ]) + isFetched, + isError, + isEmpty, + lastFetchedAt, + data, + feed, + feedType, + feedUriOrActorDid, + feedTab, + hasSession, + showProgressIntersitial, + trendingDisabled, + trendingVideoDisabled, + rightNavVisible, + gtMobile, + isVideoFeed, + areVideoFeedsEnabled, + hasPressedShowLessUris, + ageAssuranceBannerState, + isCurrentFeedAtStartupSelected, + gate, + blockedOrMutedAuthors, +]) - // events - // = +// events +// = - const onRefresh = useCallback(async () => { - logEvent('feed:refresh', { - feedType: feedType, - feedUrl: feed, - reason: 'pull-to-refresh', - }) - setIsPTRing(true) - try { - await refetch() - onHasNew?.(false) - } catch (err) { - logger.error('Failed to refresh posts feed', {message: err}) - } - setIsPTRing(false) - }, [refetch, setIsPTRing, onHasNew, feed, feedType]) +const onRefresh = useCallback(async () => { + logEvent('feed:refresh', { + feedType: feedType, + feedUrl: feed, + reason: 'pull-to-refresh', + }) + setIsPTRing(true) + try { + await refetch() + onHasNew?.(false) + } catch (err) { + logger.error('Failed to refresh posts feed', { message: err }) + } + setIsPTRing(false) +}, [refetch, setIsPTRing, onHasNew, feed, feedType]) - const onEndReached = useCallback(async () => { - if (isFetching || !hasNextPage || isError) return +const onEndReached = useCallback(async () => { + if (isFetching || !hasNextPage || isError) return - logEvent('feed:endReached', { - feedType: feedType, - feedUrl: feed, - itemCount: feedItems.length, - }) - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more posts', {message: err}) - } - }, [ - isFetching, - hasNextPage, - isError, - fetchNextPage, - feed, - feedType, - feedItems.length, - ]) + logEvent('feed:endReached', { + feedType: feedType, + feedUrl: feed, + itemCount: feedItems.length, + }) + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more posts', { message: err }) + } +}, [ + isFetching, + hasNextPage, + isError, + fetchNextPage, + feed, + feedType, + feedItems.length, +]) - const onPressTryAgain = useCallback(() => { - refetch() - onHasNew?.(false) - }, [refetch, onHasNew]) +const onPressTryAgain = useCallback(() => { + refetch() + onHasNew?.(false) +}, [refetch, onHasNew]) - const onPressRetryLoadMore = useCallback(() => { - fetchNextPage() - }, [fetchNextPage]) +const onPressRetryLoadMore = useCallback(() => { + fetchNextPage() +}, [fetchNextPage]) - // rendering - // = +// rendering +// = - const renderItem = useCallback( - ({item: row, index: rowIndex}: ListRenderItemInfo) => { - if (row.type === 'empty') { - return renderEmptyState() - } else if (row.type === 'error') { - return ( - - ) - } else if (row.type === 'loadMoreError') { - return ( - - ) - } else if (row.type === 'loading') { - return - } else if (row.type === 'feedShutdownMsg') { - return - } else if (row.type === 'interstitialFollows') { - return - } else if (row.type === 'interstitialProgressGuide') { - return - } else if (row.type === 'ageAssuranceBanner') { - return - } else if (row.type === 'interstitialTrending') { - return - } else if (row.type === 'composerPrompt') { - return - } else if (row.type === 'interstitialTrendingVideos') { - return - } else if (row.type === 'fallbackMarker') { - // HACK - // tell the user we fell back to discover - // see home.ts (feed api) for more info - // -prf - return - } else if (row.type === 'sliceItem') { - const slice = row.slice - const indexInSlice = row.indexInSlice - const item = slice.items[indexInSlice] - return ( - - ) - } else if (row.type === 'sliceViewFullThread') { - return - } else if (row.type === 'videoGridRowPlaceholder') { - return ( - - - - - - ) - } else if (row.type === 'videoGridRow') { - let sourceContext: VideoFeedSourceContext - if (feedType === 'author') { - sourceContext = { - type: 'author', - did: feedUriOrActorDid, - filter: feedTab as AuthorFilter, - } - } else { - sourceContext = { - type: 'feedgen', - uri: row.sourceFeedUri, - sourceInterstitial: feedCacheKey ?? 'none', +const renderItem = useCallback( + ({ item: row, index: rowIndex }: ListRenderItemInfo) => { + if (row.type === 'empty') { + return renderEmptyState() + } else if (row.type === 'error') { + return ( + + ) + } else if (row.type === 'loadMoreError') { + return ( + + ) + } else if (row.type === 'loading') { + return + } else if (row.type === 'feedShutdownMsg') { + return + } else if (row.type === 'interstitialFollows') { + return + } else if (row.type === 'interstitialProgressGuide') { + return + } else if (row.type === 'ageAssuranceBanner') { + return + } else if (row.type === 'interstitialTrending') { + return + } else if (row.type === 'composerPrompt') { + return + } else if (row.type === 'interstitialTrendingVideos') { + return + } else if (row.type === 'fallbackMarker') { + // HACK + // tell the user we fell back to discover + // see home.ts (feed api) for more info + // -prf + return + } else if (row.type === 'sliceItem') { + const slice = row.slice + const indexInSlice = row.indexInSlice + const item = slice.items[indexInSlice] + return ( + + ) + } else if (row.type === 'sliceViewFullThread') { + return + } else if (row.type === 'videoGridRowPlaceholder') { + return ( + + + + + + ) + } else if (row.type === 'videoGridRow') { + let sourceContext: VideoFeedSourceContext + if (feedType === 'author') { + sourceContext = { + type: 'author', + did: feedUriOrActorDid, + filter: feedTab as AuthorFilter, } - - return ( - - ) - } else if (row.type === 'showLessFollowup') { - return } else { - return null + sourceContext = { + type: 'feedgen', + uri: row.sourceFeedUri, + sourceInterstitial: feedCacheKey ?? 'none', + } } - }, - [ - renderEmptyState, - feed, - error, - onPressTryAgain, - savedFeedConfig, - _, - onPressRetryLoadMore, - feedType, - feedUriOrActorDid, - feedTab, - feedCacheKey, - onPressShowLess, - ], - ) - const shouldRenderEndOfFeed = - !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed - const FeedFooter = useCallback(() => { - /** - * A bit of padding at the bottom of the feed as you scroll and when you - * reach the end, so that content isn't cut off by the bottom of the - * screen. - */ - const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2) + return ( + + ) + } else if (row.type === 'showLessFollowup') { + return + } else { + return null + } + }, + [ + renderEmptyState, + feed, + error, + onPressTryAgain, + savedFeedConfig, + _, + onPressRetryLoadMore, + feedType, + feedUriOrActorDid, + feedTab, + feedCacheKey, + onPressShowLess, + ], +) + +const shouldRenderEndOfFeed = + !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed +const FeedFooter = useCallback(() => { + /** + * A bit of padding at the bottom of the feed as you scroll and when you + * reach the end, so that content isn't cut off by the bottom of the + * screen. + */ + const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2) - return isFetchingNextPage ? ( - - - - - ) : shouldRenderEndOfFeed ? ( - {renderEndOfFeed()} - ) : ( - - ) - }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) + return isFetchingNextPage ? ( + + + + + ) : shouldRenderEndOfFeed ? ( + {renderEndOfFeed()} + ) : ( + + ) +}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) - const liveNowConfig = useLiveNowConfig() +const liveNowConfig = useLiveNowConfig() - const seenActorWithStatusRef = useRef>(new Set()) - const seenPostUrisRef = useRef>(new Set()) +const seenActorWithStatusRef = useRef>(new Set()) +const seenPostUrisRef = useRef>(new Set()) - // Helper to calculate position in feed (count only root posts, not interstitials or thread replies) - const getPostPosition = useNonReactiveCallback( - (type: FeedRow['type'], key: string) => { - // Calculate position: find the row index in feedItems, then calculate position - const rowIndex = feedItems.findIndex( - row => row.type === 'sliceItem' && row.key === key, - ) +// Helper to calculate position in feed (count only root posts, not interstitials or thread replies) +const getPostPosition = useNonReactiveCallback( + (type: FeedRow['type'], key: string) => { + // Calculate position: find the row index in feedItems, then calculate position + const rowIndex = feedItems.findIndex( + row => row.type === 'sliceItem' && row.key === key, + ) - if (rowIndex >= 0) { - let position = 0 - for (let i = 0; i < rowIndex && i < feedItems.length; i++) { - const row = feedItems[i] - if (row.type === 'sliceItem') { - // Only count root posts (indexInSlice === 0), not thread replies - if (row.indexInSlice === 0) { - position++ - } - } else if (row.type === 'videoGridRow') { - // Count each video in the grid row - position += row.items.length + if (rowIndex >= 0) { + let position = 0 + for (let i = 0; i < rowIndex && i < feedItems.length; i++) { + const row = feedItems[i] + if (row.type === 'sliceItem') { + // Only count root posts (indexInSlice === 0), not thread replies + if (row.indexInSlice === 0) { + position++ } + } else if (row.type === 'videoGridRow') { + // Count each video in the grid row + position += row.items.length } - return position } - }, - ) + return position + } + }, +) + +const onItemSeen = useCallback( + (item: FeedRow) => { + feedFeedback.onItemSeen(item) - const onItemSeen = useCallback( - (item: FeedRow) => { - feedFeedback.onItemSeen(item) + // Track post:view events + if (item.type === 'sliceItem') { + const slice = item.slice + const indexInSlice = item.indexInSlice + const postItem = slice.items[indexInSlice] + const post = postItem.post + + // Only track the root post of each slice (index 0) to avoid double-counting thread items + if (indexInSlice === 0 && !seenPostUrisRef.current.has(post.uri)) { + seenPostUrisRef.current.add(post.uri) + + const position = getPostPosition('sliceItem', item.key) + + logger.metric( + 'post:view', + { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor: feedFeedback.feedDescriptor || feed, + position, + }, + { statsig: false }, + ) + } - // Track post:view events - if (item.type === 'sliceItem') { - const slice = item.slice - const indexInSlice = item.indexInSlice - const postItem = slice.items[indexInSlice] + // Live status tracking (existing code) + const actor = post.author + if ( + actor.status && + validateStatus(actor.did, actor.status, liveNowConfig) && + isStatusStillActive(actor.status.expiresAt) + ) { + if (!seenActorWithStatusRef.current.has(actor.did)) { + seenActorWithStatusRef.current.add(actor.did) + logger.metric( + 'live:view:post', + { + subject: actor.did, + feed, + }, + { statsig: false }, + ) + } + } + } else if (item.type === 'videoGridRow') { + // Track each video in the grid row + for (let i = 0; i < item.items.length; i++) { + const postItem = item.items[i] const post = postItem.post - // Only track the root post of each slice (index 0) to avoid double-counting thread items - if (indexInSlice === 0 && !seenPostUrisRef.current.has(post.uri)) { + if (!seenPostUrisRef.current.has(post.uri)) { seenPostUrisRef.current.add(post.uri) - const position = getPostPosition('sliceItem', item.key) + const position = getPostPosition('videoGridRow', item.key) logger.metric( 'post:view', @@ -944,97 +978,54 @@ let PostFeed = ({ feedDescriptor: feedFeedback.feedDescriptor || feed, position, }, - {statsig: false}, + { statsig: false }, ) } - - // Live status tracking (existing code) - const actor = post.author - if ( - actor.status && - validateStatus(actor.did, actor.status, liveNowConfig) && - isStatusStillActive(actor.status.expiresAt) - ) { - if (!seenActorWithStatusRef.current.has(actor.did)) { - seenActorWithStatusRef.current.add(actor.did) - logger.metric( - 'live:view:post', - { - subject: actor.did, - feed, - }, - {statsig: false}, - ) - } - } - } else if (item.type === 'videoGridRow') { - // Track each video in the grid row - for (let i = 0; i < item.items.length; i++) { - const postItem = item.items[i] - const post = postItem.post - - if (!seenPostUrisRef.current.has(post.uri)) { - seenPostUrisRef.current.add(post.uri) - - const position = getPostPosition('videoGridRow', item.key) - - logger.metric( - 'post:view', - { - uri: post.uri, - authorDid: post.author.did, - logContext: 'FeedItem', - feedDescriptor: feedFeedback.feedDescriptor || feed, - position, - }, - {statsig: false}, - ) - } - } } - }, - [feedFeedback, feed, liveNowConfig, getPostPosition], - ) + } + }, + [feedFeedback, feed, liveNowConfig, getPostPosition], +) - return ( - - item.key} - renderItem={renderItem} - ListFooterComponent={FeedFooter} - ListHeaderComponent={ListHeaderComponent} - refreshing={isPTRing} - onRefresh={onRefresh} - headerOffset={headerOffset} - progressViewOffset={progressViewOffset} - contentContainerStyle={{ - minHeight: Dimensions.get('window').height * 1.5, - }} - onScrolledDownChange={onScrolledDownChange} - onEndReached={onEndReached} - onEndReachedThreshold={2} // number of posts left to trigger load more - removeClippedSubviews={true} - extraData={extraData} - desktopFixedHeight={ - desktopFixedHeightOffset ? desktopFixedHeightOffset : true - } - initialNumToRender={initialNumToRenderOverride ?? initialNumToRender} - windowSize={9} - maxToRenderPerBatch={isIOS ? 5 : 1} - updateCellsBatchingPeriod={40} - onItemSeen={onItemSeen} - /> - - ) +return ( + + item.key} + renderItem={renderItem} + ListFooterComponent={FeedFooter} + ListHeaderComponent={ListHeaderComponent} + refreshing={isPTRing} + onRefresh={onRefresh} + headerOffset={headerOffset} + progressViewOffset={progressViewOffset} + contentContainerStyle={{ + minHeight: Dimensions.get('window').height * 1.5, + }} + onScrolledDownChange={onScrolledDownChange} + onEndReached={onEndReached} + onEndReachedThreshold={2} // number of posts left to trigger load more + removeClippedSubviews={true} + extraData={extraData} + desktopFixedHeight={ + desktopFixedHeightOffset ? desktopFixedHeightOffset : true + } + initialNumToRender={initialNumToRenderOverride ?? initialNumToRender} + windowSize={9} + maxToRenderPerBatch={isIOS ? 5 : 1} + updateCellsBatchingPeriod={40} + onItemSeen={onItemSeen} + /> + +) } PostFeed = memo(PostFeed) -export {PostFeed} +export { PostFeed } const styles = StyleSheet.create({ - feedFooter: {paddingTop: 20}, + feedFooter: { paddingTop: 20 }, }) export function isThreadParentAt(arr: Array, i: number) {