1383 lines
44 KiB
Diff
1383 lines
44 KiB
Diff
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.
|
|
</Trans>
|
|
</Text>
|
|
- <Button
|
|
- type="inverted"
|
|
- style={styles.emptyBtn}
|
|
- onPress={onPressFindAccounts}>
|
|
- <Text type="lg-medium" style={palInverted.text}>
|
|
- <Trans>Find accounts to follow</Trans>
|
|
- </Text>
|
|
- <FontAwesomeIcon
|
|
- icon="angle-right"
|
|
- style={palInverted.text as FontAwesomeIconStyle}
|
|
- size={14}
|
|
- />
|
|
- </Button>
|
|
-
|
|
- <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
|
|
- <Trans>You can also discover new Custom Feeds to follow.</Trans>
|
|
- </Text>
|
|
- <Button
|
|
- type="inverted"
|
|
- style={[styles.emptyBtn, s.mt10]}
|
|
- onPress={onPressDiscoverFeeds}>
|
|
- <Text type="lg-medium" style={palInverted.text}>
|
|
- <Trans>Discover new custom feeds</Trans>
|
|
- </Text>
|
|
- <FontAwesomeIcon
|
|
- icon="angle-right"
|
|
- style={palInverted.text as FontAwesomeIconStyle}
|
|
- size={14}
|
|
- />
|
|
- </Button>
|
|
</View>
|
|
</View>
|
|
)
|
|
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 },
|
|
]}>
|
|
<View style={styles.inner}>
|
|
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
|
|
@@ -46,36 +46,6 @@ export function FollowingEndOfFeed() {
|
|
follow.
|
|
</Trans>
|
|
</Text>
|
|
- <Button
|
|
- type="inverted"
|
|
- style={styles.emptyBtn}
|
|
- onPress={onPressFindAccounts}>
|
|
- <Text type="lg-medium" style={palInverted.text}>
|
|
- <Trans>Find accounts to follow</Trans>
|
|
- </Text>
|
|
- <FontAwesomeIcon
|
|
- icon="angle-right"
|
|
- style={palInverted.text as FontAwesomeIconStyle}
|
|
- size={14}
|
|
- />
|
|
- </Button>
|
|
-
|
|
- <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
|
|
- <Trans>You can also discover new Custom Feeds to follow.</Trans>
|
|
- </Text>
|
|
- <Button
|
|
- type="inverted"
|
|
- style={[styles.emptyBtn, s.mt10]}
|
|
- onPress={onPressDiscoverFeeds}>
|
|
- <Text type="lg-medium" style={palInverted.text}>
|
|
- <Trans>Discover new custom feeds</Trans>
|
|
- </Text>
|
|
- <FontAwesomeIcon
|
|
- icon="angle-right"
|
|
- style={palInverted.text as FontAwesomeIconStyle}
|
|
- size={14}
|
|
- />
|
|
- </Button>
|
|
</View>
|
|
</View>
|
|
)
|
|
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<number>(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<FeedRow, {type: 'sliceItem'}>) => {
|
|
+ const sliceItem = (row: Extract<FeedRow, { type: 'sliceItem' }>) => {
|
|
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<FeedRow>) => {
|
|
- if (row.type === 'empty') {
|
|
- return renderEmptyState()
|
|
- } else if (row.type === 'error') {
|
|
- return (
|
|
- <PostFeedErrorMessage
|
|
- feedDesc={feed}
|
|
- error={error ?? undefined}
|
|
- onPressTryAgain={onPressTryAgain}
|
|
- savedFeedConfig={savedFeedConfig}
|
|
- />
|
|
- )
|
|
- } else if (row.type === 'loadMoreError') {
|
|
- return (
|
|
- <LoadMoreRetryBtn
|
|
- label={_(
|
|
- msg`There was an issue fetching posts. Tap here to try again.`,
|
|
- )}
|
|
- onPress={onPressRetryLoadMore}
|
|
- />
|
|
- )
|
|
- } else if (row.type === 'loading') {
|
|
- return <PostFeedLoadingPlaceholder />
|
|
- } else if (row.type === 'feedShutdownMsg') {
|
|
- return <FeedShutdownMsg feedUri={feedUriOrActorDid} />
|
|
- } else if (row.type === 'interstitialFollows') {
|
|
- return <SuggestedFollows feed={feed} />
|
|
- } else if (row.type === 'interstitialProgressGuide') {
|
|
- return <ProgressGuide />
|
|
- } else if (row.type === 'ageAssuranceBanner') {
|
|
- return <AgeAssuranceDismissibleFeedBanner />
|
|
- } else if (row.type === 'interstitialTrending') {
|
|
- return <TrendingInterstitial />
|
|
- } else if (row.type === 'composerPrompt') {
|
|
- return <ComposerPrompt />
|
|
- } else if (row.type === 'interstitialTrendingVideos') {
|
|
- return <TrendingVideosInterstitial />
|
|
- } else if (row.type === 'fallbackMarker') {
|
|
- // HACK
|
|
- // tell the user we fell back to discover
|
|
- // see home.ts (feed api) for more info
|
|
- // -prf
|
|
- return <DiscoverFallbackHeader />
|
|
- } else if (row.type === 'sliceItem') {
|
|
- const slice = row.slice
|
|
- const indexInSlice = row.indexInSlice
|
|
- const item = slice.items[indexInSlice]
|
|
- return (
|
|
- <PostFeedItem
|
|
- post={item.post}
|
|
- record={item.record}
|
|
- reason={indexInSlice === 0 ? slice.reason : undefined}
|
|
- feedContext={slice.feedContext}
|
|
- reqId={slice.reqId}
|
|
- moderation={item.moderation}
|
|
- parentAuthor={item.parentAuthor}
|
|
- showReplyTo={row.showReplyTo}
|
|
- isThreadParent={isThreadParentAt(slice.items, indexInSlice)}
|
|
- isThreadChild={isThreadChildAt(slice.items, indexInSlice)}
|
|
- isThreadLastChild={
|
|
- isThreadChildAt(slice.items, indexInSlice) &&
|
|
- slice.items.length === indexInSlice + 1
|
|
- }
|
|
- isParentBlocked={item.isParentBlocked}
|
|
- isParentNotFound={item.isParentNotFound}
|
|
- hideTopBorder={rowIndex === 0 && indexInSlice === 0}
|
|
- rootPost={slice.items[0].post}
|
|
- onShowLess={onPressShowLess}
|
|
- />
|
|
- )
|
|
- } else if (row.type === 'sliceViewFullThread') {
|
|
- return <ViewFullThread uri={row.uri} />
|
|
- } else if (row.type === 'videoGridRowPlaceholder') {
|
|
- return (
|
|
- <View>
|
|
- <PostFeedVideoGridRowPlaceholder />
|
|
- <PostFeedVideoGridRowPlaceholder />
|
|
- <PostFeedVideoGridRowPlaceholder />
|
|
- </View>
|
|
- )
|
|
- } 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<FeedRow>) => {
|
|
+ if (row.type === 'empty') {
|
|
+ return renderEmptyState()
|
|
+ } else if (row.type === 'error') {
|
|
+ return (
|
|
+ <PostFeedErrorMessage
|
|
+ feedDesc={feed}
|
|
+ error={error ?? undefined}
|
|
+ onPressTryAgain={onPressTryAgain}
|
|
+ savedFeedConfig={savedFeedConfig}
|
|
+ />
|
|
+ )
|
|
+ } else if (row.type === 'loadMoreError') {
|
|
+ return (
|
|
+ <LoadMoreRetryBtn
|
|
+ label={_(
|
|
+ msg`There was an issue fetching posts. Tap here to try again.`,
|
|
+ )}
|
|
+ onPress={onPressRetryLoadMore}
|
|
+ />
|
|
+ )
|
|
+ } else if (row.type === 'loading') {
|
|
+ return <PostFeedLoadingPlaceholder />
|
|
+ } else if (row.type === 'feedShutdownMsg') {
|
|
+ return <FeedShutdownMsg feedUri={feedUriOrActorDid} />
|
|
+ } else if (row.type === 'interstitialFollows') {
|
|
+ return <SuggestedFollows feed={feed} />
|
|
+ } else if (row.type === 'interstitialProgressGuide') {
|
|
+ return <ProgressGuide />
|
|
+ } else if (row.type === 'ageAssuranceBanner') {
|
|
+ return <AgeAssuranceDismissibleFeedBanner />
|
|
+ } else if (row.type === 'interstitialTrending') {
|
|
+ return <TrendingInterstitial />
|
|
+ } else if (row.type === 'composerPrompt') {
|
|
+ return <ComposerPrompt />
|
|
+ } else if (row.type === 'interstitialTrendingVideos') {
|
|
+ return <TrendingVideosInterstitial />
|
|
+ } else if (row.type === 'fallbackMarker') {
|
|
+ // HACK
|
|
+ // tell the user we fell back to discover
|
|
+ // see home.ts (feed api) for more info
|
|
+ // -prf
|
|
+ return <DiscoverFallbackHeader />
|
|
+ } else if (row.type === 'sliceItem') {
|
|
+ const slice = row.slice
|
|
+ const indexInSlice = row.indexInSlice
|
|
+ const item = slice.items[indexInSlice]
|
|
+ return (
|
|
+ <PostFeedItem
|
|
+ post={item.post}
|
|
+ record={item.record}
|
|
+ reason={indexInSlice === 0 ? slice.reason : undefined}
|
|
+ feedContext={slice.feedContext}
|
|
+ reqId={slice.reqId}
|
|
+ moderation={item.moderation}
|
|
+ parentAuthor={item.parentAuthor}
|
|
+ showReplyTo={row.showReplyTo}
|
|
+ isThreadParent={isThreadParentAt(slice.items, indexInSlice)}
|
|
+ isThreadChild={isThreadChildAt(slice.items, indexInSlice)}
|
|
+ isThreadLastChild={
|
|
+ isThreadChildAt(slice.items, indexInSlice) &&
|
|
+ slice.items.length === indexInSlice + 1
|
|
}
|
|
+ isParentBlocked={item.isParentBlocked}
|
|
+ isParentNotFound={item.isParentNotFound}
|
|
+ hideTopBorder={rowIndex === 0 && indexInSlice === 0}
|
|
+ rootPost={slice.items[0].post}
|
|
+ onShowLess={onPressShowLess}
|
|
+ />
|
|
+ )
|
|
+ } else if (row.type === 'sliceViewFullThread') {
|
|
+ return <ViewFullThread uri={row.uri} />
|
|
+ } else if (row.type === 'videoGridRowPlaceholder') {
|
|
+ return (
|
|
+ <View>
|
|
+ <PostFeedVideoGridRowPlaceholder />
|
|
+ <PostFeedVideoGridRowPlaceholder />
|
|
+ <PostFeedVideoGridRowPlaceholder />
|
|
+ </View>
|
|
+ )
|
|
+ } else if (row.type === 'videoGridRow') {
|
|
+ let sourceContext: VideoFeedSourceContext
|
|
+ if (feedType === 'author') {
|
|
+ sourceContext = {
|
|
+ type: 'author',
|
|
+ did: feedUriOrActorDid,
|
|
+ filter: feedTab as AuthorFilter,
|
|
}
|
|
-
|
|
- return (
|
|
- <PostFeedVideoGridRow
|
|
- items={row.items}
|
|
- sourceContext={sourceContext}
|
|
- />
|
|
- )
|
|
- } else if (row.type === 'showLessFollowup') {
|
|
- return <ShowLessFollowup />
|
|
} 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 (
|
|
+ <PostFeedVideoGridRow
|
|
+ items={row.items}
|
|
+ sourceContext={sourceContext}
|
|
+ />
|
|
+ )
|
|
+ } else if (row.type === 'showLessFollowup') {
|
|
+ return <ShowLessFollowup />
|
|
+ } 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 ? (
|
|
- <View style={[styles.feedFooter]}>
|
|
- <ActivityIndicator />
|
|
- <View style={{height: offset}} />
|
|
- </View>
|
|
- ) : shouldRenderEndOfFeed ? (
|
|
- <View style={{minHeight: offset}}>{renderEndOfFeed()}</View>
|
|
- ) : (
|
|
- <View style={{height: offset}} />
|
|
- )
|
|
- }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
|
|
+ return isFetchingNextPage ? (
|
|
+ <View style={[styles.feedFooter]}>
|
|
+ <ActivityIndicator />
|
|
+ <View style={{ height: offset }} />
|
|
+ </View>
|
|
+ ) : shouldRenderEndOfFeed ? (
|
|
+ <View style={{ minHeight: offset }}>{renderEndOfFeed()}</View>
|
|
+ ) : (
|
|
+ <View style={{ height: offset }} />
|
|
+ )
|
|
+}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
|
|
|
|
- const liveNowConfig = useLiveNowConfig()
|
|
+const liveNowConfig = useLiveNowConfig()
|
|
|
|
- const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
|
- const seenPostUrisRef = useRef<Set<string>>(new Set())
|
|
+const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
|
+const seenPostUrisRef = useRef<Set<string>>(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 (
|
|
- <View testID={testID} style={style}>
|
|
- <List
|
|
- testID={testID ? `${testID}-flatlist` : undefined}
|
|
- ref={scrollElRef}
|
|
- data={feedItems}
|
|
- keyExtractor={item => 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}
|
|
- />
|
|
- </View>
|
|
- )
|
|
+return (
|
|
+ <View testID={testID} style={style}>
|
|
+ <List
|
|
+ testID={testID ? `${testID}-flatlist` : undefined}
|
|
+ ref={scrollElRef}
|
|
+ data={feedItems}
|
|
+ keyExtractor={item => 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}
|
|
+ />
|
|
+ </View>
|
|
+)
|
|
}
|
|
PostFeed = memo(PostFeed)
|
|
-export {PostFeed}
|
|
+export { PostFeed }
|
|
|
|
const styles = StyleSheet.create({
|
|
- feedFooter: {paddingTop: 20},
|
|
+ feedFooter: { paddingTop: 20 },
|
|
})
|
|
|
|
export function isThreadParentAt<T>(arr: Array<T>, i: number) {
|