From 483ff5b21362d4a71517ad205cc07295c4e33a11 Mon Sep 17 00:00:00 2001 From: syui Date: Sun, 7 Dec 2025 22:38:16 +0900 Subject: [PATCH] fix --- .../008-social-app-ios-policy-tos-error.patch | 62 +- .../021-social-app-ios-clean-feed.patch | 1397 ++++++++++++++++- 2 files changed, 1423 insertions(+), 36 deletions(-) diff --git a/ios/patching/008-social-app-ios-policy-tos-error.patch b/ios/patching/008-social-app-ios-policy-tos-error.patch index 83015ec..9fa76b9 100644 --- a/ios/patching/008-social-app-ios-policy-tos-error.patch +++ b/ios/patching/008-social-app-ios-policy-tos-error.patch @@ -1,8 +1,8 @@ diff --git a/src/view/screens/PrivacyPolicy.tsx b/src/view/screens/PrivacyPolicy.tsx -index a89eaadc4..1da393f03 100644 +index a89eaadc4..787a1409b 100644 --- a/src/view/screens/PrivacyPolicy.tsx +++ b/src/view/screens/PrivacyPolicy.tsx -@@ -1,52 +1,13 @@ +@@ -1,51 +1,28 @@ import React from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' @@ -19,7 +19,7 @@ index a89eaadc4..1da393f03 100644 -import {TextLink} from '#/view/com/util/Link' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -+import { WebView } from 'react-native-webview' ++import { ScrollView } from 'react-native' import * as Layout from '#/components/Layout' -import {ViewHeader} from '../com/util/ViewHeader' - @@ -28,16 +28,19 @@ index a89eaadc4..1da393f03 100644 - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() -- ++import { useSetTitle } from '#/lib/hooks/useSetTitle' ++import { atoms as a, useTheme } from '#/alf' ++import { Text } from '#/components/Typography' + - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) -+import {useSetTitle} from '#/lib/hooks/useSetTitle' - +export function PrivacyPolicyScreen() { + useSetTitle('Privacy Policy') ++ const t = useTheme() + return ( - @@ -55,16 +58,26 @@ index a89eaadc4..1da393f03 100644 - - - -- -+ ++ ++ Privacy Policy ++ ++ ++ Please refer to the following page for the Privacy Policy. ++ ++ ++ ++ https://syu.is/about/support/privacy-policy ++ + ) - } diff --git a/src/view/screens/TermsOfService.tsx b/src/view/screens/TermsOfService.tsx -index d843c713c..b81767bd5 100644 +index d843c713c..28333cc5b 100644 --- a/src/view/screens/TermsOfService.tsx +++ b/src/view/screens/TermsOfService.tsx -@@ -1,50 +1,13 @@ +@@ -1,49 +1,28 @@ import React from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' @@ -81,7 +94,7 @@ index d843c713c..b81767bd5 100644 -import {TextLink} from '#/view/com/util/Link' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -+import { WebView } from 'react-native-webview' ++import { ScrollView } from 'react-native' import * as Layout from '#/components/Layout' -import {ViewHeader} from '../com/util/ViewHeader' - @@ -90,16 +103,19 @@ index d843c713c..b81767bd5 100644 - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() -- ++import { useSetTitle } from '#/lib/hooks/useSetTitle' ++import { atoms as a, useTheme } from '#/alf' ++import { Text } from '#/components/Typography' + - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) -+import {useSetTitle} from '#/lib/hooks/useSetTitle' - +export function TermsOfServiceScreen() { + useSetTitle('Terms of Service') ++ const t = useTheme() + return ( - @@ -115,8 +131,18 @@ index d843c713c..b81767bd5 100644 - - - -- -+ ++ ++ Terms of Service ++ ++ ++ Please refer to the following page for the Terms of Service. ++ ++ ++ ++ https://syu.is/about/support/tos ++ + ) - } diff --git a/ios/patching/021-social-app-ios-clean-feed.patch b/ios/patching/021-social-app-ios-clean-feed.patch index b1be019..d96eb5b 100644 --- a/ios/patching/021-social-app-ios-clean-feed.patch +++ b/ios/patching/021-social-app-ios-clean-feed.patch @@ -1,21 +1,1382 @@ -diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx -index e058e2883..e762b1418 100644 ---- a/src/view/screens/Home.tsx -+++ b/src/view/screens/Home.tsx -@@ -39,6 +39,16 @@ import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' - import * as Layout from '#/components/Layout' - import {useDemoMode} from '#/storage/hooks/demo-mode' +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' -+const DEFAULT_PINNED_FEEDS = [{ -+ feedDescriptor: 'following', -+ displayName: 'Following', -+ id: 'following', -+ type: 'feed', -+ savedFeed: undefined, -+ pinned: true, -+}] +-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 ++ } ++ }, ++) + - type Props = NativeStackScreenProps - export function HomeScreen(props: Props) { - const {setShowLoggedOut} = useLoggedOutViewControls() ++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) {