From 2c0e8136bc489cf36a6b12a55c9d5bf8fab4ace7 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jan 2025 13:13:21 +0000 Subject: [PATCH 01/59] yolo interstitial --- src/components/FeedInterstitials.tsx | 46 ++++++++++++++++++++- src/components/LinearGradientBackground.tsx | 14 +++++-- src/lib/constants.ts | 2 + src/view/com/posts/PostFeed.tsx | 36 +++++++++++++--- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 268a5ff5b8..3c6c4f53f4 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -18,14 +18,16 @@ import * as userActionHistory from '#/state/userActionHistory' import {SeenPost} from '#/state/userActionHistory' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' -import {Button} from '#/components/Button' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {InlineLinkText} from '#/components/Link' +import {InlineLinkText, Link} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' +import {LinearGradientBackground} from './LinearGradientBackground' import {ProgressGuideList} from './ProgressGuide/List' const MOBILE_CARD_WIDTH = 300 @@ -518,3 +520,43 @@ export function ProgressGuide() { ) } + +export function VideoModeEntranceInterstitial() { + const {_} = useLingui() + const t = useTheme() + + return ( + + + + + NEW! + {' '} + Scroll this feed in video mode + + + + Start watching + + + + + + ) +} diff --git a/src/components/LinearGradientBackground.tsx b/src/components/LinearGradientBackground.tsx index 724df43f31..9b28b897c0 100644 --- a/src/components/LinearGradientBackground.tsx +++ b/src/components/LinearGradientBackground.tsx @@ -6,12 +6,18 @@ import {gradients} from '#/alf/tokens' export function LinearGradientBackground({ style, + gradient = 'sky', children, + start, + end, }: { - style: StyleProp - children: React.ReactNode + style?: StyleProp + gradient?: keyof typeof gradients + children?: React.ReactNode + start?: [number, number] + end?: [number, number] }) { - const gradient = gradients.sky.values.map(([_, color]) => { + const colors = gradients[gradient].values.map(([_, color]) => { return color }) as [string, string, ...string[]] @@ -20,7 +26,7 @@ export function LinearGradientBackground({ } return ( - + {children} ) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 5ae000f729..82936ed55a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -124,6 +124,8 @@ export const BSKY_FEED_OWNER_DIDS = [ export const DISCOVER_FEED_URI = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' +export const THEVIDS_FEED_URI = + 'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids' export const DISCOVER_SAVED_FEED = { type: 'feed', value: DISCOVER_FEED_URI, diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index f9b2e6e76b..e2cae4d9bd 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -14,7 +14,11 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' +import { + DISCOVER_FEED_URI, + KNOWN_SHUTDOWN_FEEDS, + THEVIDS_FEED_URI, +} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig' @@ -35,12 +39,16 @@ import { } from '#/state/queries/post-feed' import {useSession} from '#/state/session' import {useProgressGuide} from '#/state/shell/progress-guide' +import {List, ListRef} from '#/view/com/util/List' +import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {useBreakpoints} from '#/alf' -import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' +import { + ProgressGuide, + SuggestedFollows, + VideoModeEntranceInterstitial, +} from '#/components/FeedInterstitials' import {TrendingInterstitial} from '#/components/interstitials/Trending' -import {List, ListRef} from '../util/List' -import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' -import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' import {PostFeedErrorMessage} from './PostFeedErrorMessage' @@ -97,6 +105,10 @@ type FeedRow = type: 'interstitialTrending' key: string } + | { + type: 'videoModeEntrance' + key: string + } export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null { if (feedRow.type === 'sliceItem') { @@ -270,11 +282,13 @@ let PostFeed = ({ const {trendingDisabled} = useTrendingSettings() const feedItems: FeedRow[] = React.useMemo(() => { - let feedKind: 'following' | 'discover' | 'profile' | undefined + let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined if (feedType === 'following') { feedKind = 'following' } else if (feedUri === DISCOVER_FEED_URI) { feedKind = 'discover' + } else if (feedUri === THEVIDS_FEED_URI) { + feedKind = 'thevids' } else if ( feedType === 'author' && (feedTab === 'posts_and_author_threads' || @@ -335,6 +349,14 @@ let PostFeed = ({ key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, }) } + } else if (feedKind === 'thevids') { + if (sliceIndex === 0) { + arr.push({ + type: 'videoModeEntrance', + key: + 'videoModeEntrance-' + sliceIndex + '-' + lastFetchedAt, + }) + } } } @@ -498,6 +520,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'videoModeEntrance') { + return } else if (row.type === 'sliceItem') { const slice = row.slice if (slice.isFallbackMarker) { From 25b5579bb3eb8c62e16acb3c2d3b46f63bdb2192 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jan 2025 19:37:34 +0000 Subject: [PATCH 02/59] yolo mode --- bskyweb/cmd/bskyweb/server.go | 3 + src/Navigation.tsx | 6 + src/components/FeedInterstitials.tsx | 2 +- src/components/Layout/Header/index.tsx | 6 +- src/lib/routes/types.ts | 1 + src/routes.ts | 2 + src/screens/Feeds/YoloScreen.tsx | 216 +++++++++++++++++++++++++ src/view/com/util/List.tsx | 6 +- 8 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 src/screens/Feeds/YoloScreen.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 3ef19fc462..5febbc9d05 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -272,6 +272,9 @@ func serve(cctx *cli.Context) error { e.GET("/messages", server.WebGeneric) e.GET("/messages/:conversation", server.WebGeneric) + // temp + e.GET("/temp-yolo", server.WebGeneric) + // profile endpoints; only first populates info e.GET("/profile/:handleOrDID", server.WebProfile) e.GET("/profile/:handleOrDID/follows", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 18705c5ffb..494f0148f2 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -89,6 +89,7 @@ import {Wizard} from '#/screens/StarterPack/Wizard' import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {YoloScreen} from './screens/Feeds/YoloScreen' import {AboutSettingsScreen} from './screens/Settings/AboutSettings' import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings' @@ -422,6 +423,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => Wizard} options={{title: title(msg`Edit your starter pack`), requireAuth: true}} /> + YoloScreen} + options={{title: title(msg`Yolo mode`), requireAuth: true}} + /> ) } diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 3c6c4f53f4..77d1986947 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -546,7 +546,7 @@ export function VideoModeEntranceInterstitial() { ) { shape="square" onPress={onPressBack} hitSlop={HITSLOP_30} - style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]} + style={[ + {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, + a.bg_transparent, + style, + ]} {...props}> diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index d720886e9f..2037feee25 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -57,6 +57,7 @@ export type CommonNavigatorParams = { StarterPackShort: {code: string} StarterPackWizard: undefined StarterPackEdit: {rkey?: string} + TempYolo: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index 7cd7c0880d..ab05bcb9b2 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -64,4 +64,6 @@ export const router = new Router({ StarterPack: '/starter-pack/:name/:rkey', StarterPackShort: '/starter-pack-short/:code', StarterPackWizard: '/starter-pack/create', + // temp + TempYolo: '/temp-yolo', }) diff --git a/src/screens/Feeds/YoloScreen.tsx b/src/screens/Feeds/YoloScreen.tsx new file mode 100644 index 0000000000..4e68724d1c --- /dev/null +++ b/src/screens/Feeds/YoloScreen.tsx @@ -0,0 +1,216 @@ +import {useCallback, useEffect, useState} from 'react' +import { + ListRenderItem, + SafeAreaView, + useWindowDimensions, + View, + ViewToken, +} from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {useEvent} from 'expo' +import {BlurView} from 'expo-blur' +import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' +import {AppBskyEmbedVideo, AppBskyFeedDefs} from '@atproto/api' +import {Trans} from '@lingui/macro' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {THEVIDS_FEED_URI} from '#/lib/constants' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {FeedPostSliceItem, usePostFeedQuery} from '#/state/queries/post-feed' +import {useSetMinimalShellMode} from '#/state/shell' +import {List} from '#/view/com/util/List' +import {atoms as a, useTheme} from '#/alf' +import * as Layout from '#/components/Layout' +import {ListFooter} from '#/components/Lists' + +type Props = NativeStackScreenProps +export function YoloScreen({}: Props) { + const {top} = useSafeAreaInsets() + const t = useTheme() + const [headerHeight, setHeaderHeight] = useState(0) + + const setMinShellMode = useSetMinimalShellMode() + useFocusEffect( + useCallback(() => { + setMinShellMode(true) + return () => { + setMinShellMode(false) + } + }, [setMinShellMode]), + ) + + return ( + + + setHeaderHeight(nativeEvent.layout.height) + }> + + + + + Yolo mode + + + + + + + + ) +} + +function YoloFeed({headerHeight}: {headerHeight: number}) { + const { + data, + isFetching, + refetch, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = usePostFeedQuery(`feedgen|${THEVIDS_FEED_URI}`) + + const player1 = useVideoPlayer('', p => { + p.loop = true + }) + const player2 = useVideoPlayer('', p => { + p.loop = true + }) + const player3 = useVideoPlayer('', p => { + p.loop = true + }) + + const videos = data?.pages.flatMap(page => + page.slices.flatMap(slice => slice.items), + ) + + const [currentIndex, setCurrentIndex] = useState(0) + + const renderItem: ListRenderItem = useCallback( + ({item, index}) => { + const {post} = item + if (!post.embed || !AppBskyEmbedVideo.isView(post.embed)) { + return null + } + + const player = [player1, player2, player3][index % 3] + + return ( + + ) + }, + [player1, player2, player3, currentIndex, headerHeight], + ) + + const onViewableItemsChanged = useCallback( + ({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { + if (viewableItems[0] && viewableItems[0].index !== null) { + setCurrentIndex(viewableItems[0].index) + } + }, + [], + ) + + return ( + + } + onEndReached={() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }} + showsVerticalScrollIndicator={false} + onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={{itemVisiblePercentThreshold: 75}} + /> + ) +} + +function keyExtractor(item: FeedPostSliceItem) { + return item._reactKey +} + +function VideoScreen({ + player, + embed, + active, + loaded, + headerHeight, +}: { + player: VideoPlayer + post: AppBskyFeedDefs.PostView + embed: AppBskyEmbedVideo.View + active: boolean + loaded: boolean + headerHeight: number +}) { + const {height, width} = useWindowDimensions() + const source = embed.playlist + const sourceChangeEvent = useEvent(player, 'sourceChange') as { + // incorrect types + source: {uri?: string} + oldSource: {uri?: string} + } + + useEffect(() => { + if (loaded && sourceChangeEvent?.source?.uri !== source) { + player.replace(source) + } + }, [sourceChangeEvent?.source?.uri, loaded, source, player]) + + useEffect(() => { + if (active) { + player.play() + } + return () => { + player.pause() + } + }, [active, player]) + + return ( + + + {active && ( + + )} + + + ) +} diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index a4e1a0947b..41ca5b5725 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -152,6 +152,9 @@ let List = React.forwardRef( return ( ( onScroll={scrollHandler} scrollsToTop={!activeLightbox} scrollEventThrottle={1} - onViewableItemsChanged={onViewableItemsChanged} - viewabilityConfig={viewabilityConfig} - showsVerticalScrollIndicator={!isAndroid} style={style} // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn ref={ref} From 530db147e853cbe8405a76ae2f1081821fa2f775 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 15 Jan 2025 14:35:25 +0000 Subject: [PATCH 03/59] right swipe --- bskyweb/cmd/bskyweb/server.go | 2 +- src/Navigation.tsx | 11 +- src/components/FeedInterstitials.tsx | 2 +- src/lib/constants.ts | 2 +- src/lib/routes/types.ts | 2 +- src/routes.ts | 2 +- .../Feeds/{YoloScreen.tsx => VibeScreen.tsx} | 164 +++++++++++++----- src/view/com/posts/PostFeed.tsx | 4 +- 8 files changed, 133 insertions(+), 56 deletions(-) rename src/screens/Feeds/{YoloScreen.tsx => VibeScreen.tsx} (51%) diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 5febbc9d05..12ba3a15eb 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -273,7 +273,7 @@ func serve(cctx *cli.Context) error { e.GET("/messages/:conversation", server.WebGeneric) // temp - e.GET("/temp-yolo", server.WebGeneric) + e.GET("/temp-vibe", server.WebGeneric) // profile endpoints; only first populates info e.GET("/profile/:handleOrDID", server.WebProfile) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 494f0148f2..acb8d7fc46 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -89,7 +89,7 @@ import {Wizard} from '#/screens/StarterPack/Wizard' import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' -import {YoloScreen} from './screens/Feeds/YoloScreen' +import {VibeScreen} from './screens/Feeds/VibeScreen' import {AboutSettingsScreen} from './screens/Settings/AboutSettings' import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings' @@ -424,9 +424,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { options={{title: title(msg`Edit your starter pack`), requireAuth: true}} /> YoloScreen} - options={{title: title(msg`Yolo mode`), requireAuth: true}} + name="TempVibe" + getComponent={() => VibeScreen} + options={{ + title: title(msg`Vibe`), + requireAuth: true, + }} /> ) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 77d1986947..9f1505cc98 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -546,7 +546,7 @@ export function VideoModeEntranceInterstitial() { -export function YoloScreen({}: Props) { +type Props = NativeStackScreenProps +export function VibeScreen({}: Props) { const {top} = useSafeAreaInsets() - const t = useTheme() const [headerHeight, setHeaderHeight] = useState(0) const setMinShellMode = useSetMinimalShellMode() @@ -40,37 +47,41 @@ export function YoloScreen({}: Props) { }, [setMinShellMode]), ) + useSetLightStatusBar(true) + return ( - - - setHeaderHeight(nativeEvent.layout.height) - }> - - - - - Yolo mode - - - - - - - + + + + setHeaderHeight(nativeEvent.layout.height) + }> + + + + + {/* TODO: needs to be feed name */} + Vibes (wip) + + + + + + + + ) } function YoloFeed({headerHeight}: {headerHeight: number}) { + const isFocused = useIsFocused() const { data, isFetching, @@ -78,7 +89,7 @@ function YoloFeed({headerHeight}: {headerHeight: number}) { hasNextPage, isFetchingNextPage, fetchNextPage, - } = usePostFeedQuery(`feedgen|${THEVIDS_FEED_URI}`) + } = usePostFeedQuery(`feedgen|${VIBES_FEED_URI}`) const player1 = useVideoPlayer('', p => { p.loop = true @@ -106,17 +117,17 @@ function YoloFeed({headerHeight}: {headerHeight: number}) { const player = [player1, player2, player3][index % 3] return ( - ) }, - [player1, player2, player3, currentIndex, headerHeight], + [player1, player2, player3, currentIndex, headerHeight, isFocused], ) const onViewableItemsChanged = useCallback( @@ -159,7 +170,7 @@ function keyExtractor(item: FeedPostSliceItem) { return item._reactKey } -function VideoScreen({ +function VibeItem({ player, embed, active, @@ -181,17 +192,34 @@ function VideoScreen({ oldSource: {uri?: string} } + // for initial video - useEffect will handle the typical case where + // videos have a chance to preload + const maybePlay = useNonReactiveCallback(() => { + if (active && !player.playing) { + console.log('play (nonreactive)') + player.play() + } + }) + useEffect(() => { if (loaded && sourceChangeEvent?.source?.uri !== source) { player.replace(source) + // play next tick + const timeout = setTimeout(() => { + maybePlay() + }, 0) + return () => { + clearTimeout(timeout) + } } - }, [sourceChangeEvent?.source?.uri, loaded, source, player]) + }, [sourceChangeEvent?.source?.uri, loaded, source, player, maybePlay]) useEffect(() => { if (active) { + console.log('play (effect)') player.play() - } - return () => { + } else { + // should be a cleanup function, but that causes a crash player.pause() } }, [active, player]) @@ -202,7 +230,9 @@ function VideoScreen({ height, width, }}> - + {active && ( )} + ) } + +function VibeOverlay({player}: {player: VideoPlayer}) { + const navigation = useNavigation() + const pushToProfile = useNonReactiveCallback(() => { + navigation.navigate('Profile', {name: 'lulaoficial.bsky.social'}) + }) + + const togglePlayPause = () => { + if (player.playing) { + player.pause() + } else { + player.play() + } + } + + const gesture = useMemo(() => { + const dragLeftGesture = Gesture.Pan() + .onEnd(evt => { + 'worklet' + if (evt.translationX < -100) { + runOnJS(pushToProfile)() + } + }) + .activeOffsetX([-10, 10]) + .failOffsetY([-10, 10]) + .maxPointers(1) + + return dragLeftGesture + }, [pushToProfile]) + + return ( + + + + + + ) +} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index e2cae4d9bd..a06230fd68 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -17,7 +17,7 @@ import {useQueryClient} from '@tanstack/react-query' import { DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS, - THEVIDS_FEED_URI, + VIBES_FEED_URI, } from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' @@ -287,7 +287,7 @@ let PostFeed = ({ feedKind = 'following' } else if (feedUri === DISCOVER_FEED_URI) { feedKind = 'discover' - } else if (feedUri === THEVIDS_FEED_URI) { + } else if (feedUri === VIBES_FEED_URI) { feedKind = 'thevids' } else if ( feedType === 'author' && From a4a5b57adfa017d9cf53d8896aab4e2e55e64f1a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 15 Jan 2025 14:43:01 +0000 Subject: [PATCH 04/59] fix nav gesture --- src/screens/Feeds/VibeScreen.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 68aa7adc27..bc9b5b6bde 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -262,15 +262,16 @@ function VibeOverlay({player}: {player: VideoPlayer}) { const gesture = useMemo(() => { const dragLeftGesture = Gesture.Pan() + .activeOffsetX([0, 10]) + .failOffsetX([-10, 0]) + .failOffsetY([-10, 10]) + .maxPointers(1) .onEnd(evt => { 'worklet' - if (evt.translationX < -100) { + if (evt.translationX < -50) { runOnJS(pushToProfile)() } }) - .activeOffsetX([-10, 10]) - .failOffsetY([-10, 10]) - .maxPointers(1) return dragLeftGesture }, [pushToProfile]) From 6b10ccf3da09537dfb7956363a53a4e29a6b025d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 15 Jan 2025 15:58:42 +0000 Subject: [PATCH 05/59] vibe controls --- src/components/Dialog/index.tsx | 4 +- src/lib/statsig/events.ts | 8 +- src/screens/Feeds/VibeScreen.tsx | 88 +++++++++++++++++++--- src/view/com/util/post-ctrls/PostCtrls.tsx | 2 +- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index c424321be7..597964e291 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -27,6 +27,7 @@ import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' import {List, ListMethods, ListProps} from '#/view/com/util/List' import {atoms as a, useTheme} from '#/alf' +import {useThemeName} from '#/alf/util/useColorModeTheme' import {Context, useDialogContext} from '#/components/Dialog/context' import { DialogControlProps, @@ -55,7 +56,8 @@ export function Outer({ nativeOptions, testID, }: React.PropsWithChildren) { - const t = useTheme() + const themeName = useThemeName() + const t = useTheme(themeName) const ref = React.useRef(null) const closeCallbacks = React.useRef<(() => void)[]>([]) const {setDialogIsOpen, setFullyExpandedCount} = diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 19bf06ba98..220cbd7810 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -131,16 +131,16 @@ export type LogEvents = { doesPosterFollowLiker: boolean | undefined likerClout: number | undefined postClout: number | undefined - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' } 'post:repost': { - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' } 'post:unlike': { - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' } 'post:unrepost': { - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' } 'post:mute': {} 'post:unmute': {} diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index bc9b5b6bde..d318afce3f 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -10,8 +10,15 @@ import {runOnJS} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {useEvent} from 'expo' import {BlurView} from 'expo-blur' +import {LinearGradient} from 'expo-linear-gradient' import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' -import {AppBskyEmbedVideo, AppBskyFeedDefs} from '@atproto/api' +import { + AppBskyEmbedVideo, + AppBskyFeedDefs, + AppBskyFeedPost, + AtUri, + RichText as RichTextAPI, +} from '@atproto/api' import {Trans} from '@lingui/macro' import { useFocusEffect, @@ -23,14 +30,21 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {VIBES_FEED_URI} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {POST_TOMBSTONE, usePostShadow} from '#/state/cache/post-shadow' import {FeedPostSliceItem, usePostFeedQuery} from '#/state/queries/post-feed' import {useSetMinimalShellMode} from '#/state/shell' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' -import {atoms as a, ThemeProvider} from '#/alf' +import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {Button} from '#/components/Button' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' +import {RichText} from '#/components/RichText' +import {Text} from '#/components/Typography' type Props = NativeStackScreenProps export function VibeScreen({}: Props) { @@ -121,7 +135,7 @@ function YoloFeed({headerHeight}: {headerHeight: number}) { player={player} post={post} embed={post.embed} - loaded={Math.abs(index - currentIndex) < 2} + loaded={isFocused && Math.abs(index - currentIndex) < 2} active={isFocused && index === currentIndex} headerHeight={headerHeight} /> @@ -172,6 +186,7 @@ function keyExtractor(item: FeedPostSliceItem) { function VibeItem({ player, + post, embed, active, loaded, @@ -240,16 +255,25 @@ function VibeItem({ nativeControls={false} /> )} - + ) } -function VibeOverlay({player}: {player: VideoPlayer}) { +function VibeOverlay({ + player, + post, +}: { + player: VideoPlayer + post: AppBskyFeedDefs.PostView +}) { + const postShadow = usePostShadow(post) + const insets = useSafeAreaInsets() + const t = useTheme() const navigation = useNavigation() const pushToProfile = useNonReactiveCallback(() => { - navigation.navigate('Profile', {name: 'lulaoficial.bsky.social'}) + navigation.navigate('Profile', {name: post.author.did}) }) const togglePlayPause = () => { @@ -276,16 +300,60 @@ function VibeOverlay({player}: {player: VideoPlayer}) { return dragLeftGesture }, [pushToProfile]) + const rkey = new AtUri(post.uri).rkey + const record = AppBskyFeedPost.isRecord(post.record) ? post.record : undefined + const richText = new RichTextAPI({ + text: record?.text || '', + facets: record?.facets, + }) + return ( - - + + - - + + + + + + {sanitizeDisplayName( + post.author.displayName || post.author.handle, + )} + + + {sanitizeHandle(post.author.handle, '@')} + + + + + {postShadow !== POST_TOMBSTONE && record && ( + + navigation.navigate('PostThread', {name: post.author.did, rkey}) + } + big + /> + )} + + + ) } diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 607a480ff2..e51353fd31 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -69,7 +69,7 @@ let PostCtrls = ({ style?: StyleProp onPressReply: () => void onPostReply?: (postUri: string | undefined) => void - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' threadgateRecord?: AppBskyFeedThreadgate.Record }): React.ReactNode => { const t = useTheme() From c9953c72b0a8fcdc26df6ed922311bc5eef39f39 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 15 Jan 2025 22:22:35 +0000 Subject: [PATCH 06/59] collapsible post text --- src/components/RichText.tsx | 10 +++- src/screens/Feeds/VibeScreen.tsx | 95 ++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 6d7e50e480..4edd9f88ee 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -19,7 +19,7 @@ import {Text, TextProps} from '#/components/Typography' const WORD_WRAP = {wordWrap: 1} export type RichTextProps = TextStyleProp & - Pick & { + Pick & { value: RichTextAPI | string testID?: string numberOfLines?: number @@ -43,6 +43,8 @@ export function RichText({ onLinkPress, interactiveStyle, emojiMultiplier = 1.85, + onLayout, + onTextLayout, }: RichTextProps) { const richText = React.useMemo( () => @@ -70,6 +72,8 @@ export function RichText({ selectable={selectable} testID={testID} style={[plainStyles, {fontSize}]} + onLayout={onLayout} + onTextLayout={onTextLayout} // @ts-ignore web only -prf dataSet={WORD_WRAP}> {text} @@ -83,6 +87,8 @@ export function RichText({ testID={testID} style={plainStyles} numberOfLines={numberOfLines} + onLayout={onLayout} + onTextLayout={onTextLayout} // @ts-ignore web only -prf dataSet={WORD_WRAP}> {text} @@ -163,6 +169,8 @@ export function RichText({ testID={testID} style={plainStyles} numberOfLines={numberOfLines} + onLayout={onLayout} + onTextLayout={onTextLayout} // @ts-ignore web only -prf dataSet={WORD_WRAP}> {els} diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index d318afce3f..27a35c1edc 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -1,6 +1,8 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import { + LayoutAnimation, ListRenderItem, + ScrollView, useWindowDimensions, View, ViewToken, @@ -19,7 +21,8 @@ import { AtUri, RichText as RichTextAPI, } from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import { useFocusEffect, useIsFocused, @@ -27,7 +30,7 @@ import { } from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {VIBES_FEED_URI} from '#/lib/constants' +import {HITSLOP_20, VIBES_FEED_URI} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' @@ -40,7 +43,7 @@ import {List} from '#/view/com/util/List' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, ThemeProvider, useTheme} from '#/alf' -import {Button} from '#/components/Button' +import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' import {RichText} from '#/components/RichText' @@ -272,6 +275,8 @@ function VibeOverlay({ const insets = useSafeAreaInsets() const t = useTheme() const navigation = useNavigation() + const [expanded, setExpanded] = useState(false) + const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) }) @@ -288,7 +293,7 @@ function VibeOverlay({ const dragLeftGesture = Gesture.Pan() .activeOffsetX([0, 10]) .failOffsetX([-10, 0]) - .failOffsetY([-10, 10]) + .failOffsetY([-5, 5]) .maxPointers(1) .onEnd(evt => { 'worklet' @@ -317,12 +322,16 @@ function VibeOverlay({ - + {sanitizeDisplayName( post.author.displayName || post.author.handle, )} @@ -334,12 +343,14 @@ function VibeOverlay({ - + {record?.text?.trim() && ( + + )} {postShadow !== POST_TOMBSTONE && record && ( ) } + +function ExpandableRichTextView({ + value, + authorHandle, + expanded, + setExpanded, +}: { + value: RichTextAPI + authorHandle?: string + expanded: boolean + setExpanded: React.Dispatch> +}) { + const {height: screenHeight} = useWindowDimensions() + const [constrained, setConstrained] = useState(false) + const [contentHeight, setContentHeight] = useState(0) + const {_} = useLingui() + + return ( + { + LayoutAnimation.configureNext({ + duration: 500, + update: {type: 'spring', springDamping: 0.6}, + }) + setContentHeight(h) + }} + style={{height: Math.min(contentHeight, screenHeight * 0.5)}} + contentContainerStyle={[ + a.gap_xs, + expanded ? [a.align_start] : a.flex_row, + ]}> + { + if (!constrained && evt.nativeEvent.lines.length > 1) { + setConstrained(true) + } + }} + /> + {constrained && ( + + )} + + ) +} From 9bf91ce316e875e5000ba535490d98743bac519d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 15 Jan 2025 22:45:27 +0000 Subject: [PATCH 07/59] rm blurview, cover for tall videos --- src/screens/Feeds/VibeScreen.tsx | 44 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 27a35c1edc..b80901be38 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -11,7 +11,6 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {runOnJS} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {useEvent} from 'expo' -import {BlurView} from 'expo-blur' import {LinearGradient} from 'expo-linear-gradient' import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import { @@ -52,7 +51,6 @@ import {Text} from '#/components/Typography' type Props = NativeStackScreenProps export function VibeScreen({}: Props) { const {top} = useSafeAreaInsets() - const [headerHeight, setHeaderHeight] = useState(0) const setMinShellMode = useSetMinimalShellMode() useFocusEffect( @@ -69,18 +67,13 @@ export function VibeScreen({}: Props) { return ( - - setHeaderHeight(nativeEvent.layout.height) - }> - + ]}> + @@ -90,14 +83,14 @@ export function VibeScreen({}: Props) { - - + + ) } -function YoloFeed({headerHeight}: {headerHeight: number}) { +function YoloFeed() { const isFocused = useIsFocused() const { data, @@ -140,11 +133,10 @@ function YoloFeed({headerHeight}: {headerHeight: number}) { embed={post.embed} loaded={isFocused && Math.abs(index - currentIndex) < 2} active={isFocused && index === currentIndex} - headerHeight={headerHeight} /> ) }, - [player1, player2, player3, currentIndex, headerHeight, isFocused], + [player1, player2, player3, currentIndex, isFocused], ) const onViewableItemsChanged = useCallback( @@ -193,16 +185,15 @@ function VibeItem({ embed, active, loaded, - headerHeight, }: { player: VideoPlayer post: AppBskyFeedDefs.PostView embed: AppBskyEmbedVideo.View active: boolean loaded: boolean - headerHeight: number }) { const {height, width} = useWindowDimensions() + const insets = useSafeAreaInsets() const source = embed.playlist const sourceChangeEvent = useEvent(player, 'sourceChange') as { // incorrect types @@ -214,7 +205,6 @@ function VibeItem({ // videos have a chance to preload const maybePlay = useNonReactiveCallback(() => { if (active && !player.playing) { - console.log('play (nonreactive)') player.play() } }) @@ -234,7 +224,6 @@ function VibeItem({ useEffect(() => { if (active) { - console.log('play (effect)') player.play() } else { // should be a cleanup function, but that causes a crash @@ -242,20 +231,29 @@ function VibeItem({ } }, [active, player]) + const screenAspectRatio = + (width - insets.left - insets.right) / (height - insets.bottom) + + const videoAspectRatio = + (embed.aspectRatio?.width ?? 1) / (embed.aspectRatio?.height ?? 1) + + // if the video is either taller, on only 20% shorter than the screen, + // set the video to be cover rather than contain + const isCloseEnough = videoAspectRatio < screenAspectRatio * 1.2 + return ( - - {active && ( + + {loaded && ( )} From 2db3cac6210d6c502ed15b51d37be891443abd7e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 15 Jan 2025 23:47:52 +0000 Subject: [PATCH 08/59] smarter video source handling --- src/screens/Feeds/VibeScreen.tsx | 126 +++++++++++++++++++------------ 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index b80901be38..4b9be8f3f1 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useMemo, useState} from 'react' +import {useCallback, useMemo, useState} from 'react' import { LayoutAnimation, ListRenderItem, @@ -10,7 +10,6 @@ import { import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {runOnJS} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' -import {useEvent} from 'expo' import {LinearGradient} from 'expo-linear-gradient' import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import { @@ -101,6 +100,10 @@ function YoloFeed() { fetchNextPage, } = usePostFeedQuery(`feedgen|${VIBES_FEED_URI}`) + const [currentSources, setCurrentSources] = useState< + [string | null, string | null, string | null] + >([null, null, null]) + const player1 = useVideoPlayer('', p => { p.loop = true }) @@ -125,27 +128,89 @@ function YoloFeed() { } const player = [player1, player2, player3][index % 3] + const currentSource = currentSources[index % 3] return ( ) }, - [player1, player2, player3, currentIndex, isFocused], + [player1, player2, player3, currentIndex, isFocused, currentSources], ) + const updateVideoState = useNonReactiveCallback((index: number) => { + if (!videos) return + setCurrentIndex(index) + setCurrentSources(oldSources => { + const currentSources = [...oldSources] as [ + string | null, + string | null, + string | null, + ] + + const prevEmbed = videos[index - 1]?.post.embed + const prevVideo = + prevEmbed && AppBskyEmbedVideo.isView(prevEmbed) + ? prevEmbed.playlist + : null + const currEmbed = videos[index]?.post.embed + const currVideo = + currEmbed && AppBskyEmbedVideo.isView(currEmbed) + ? currEmbed.playlist + : null + const nextEmbed = videos[index + 1]?.post.embed + const nextVideo = + nextEmbed && AppBskyEmbedVideo.isView(nextEmbed) + ? nextEmbed.playlist + : null + + const prevPlayer = [player1, player2, player3][(index + 2) % 3] + const prevPlayerCurrentSource = currentSources[(index + 2) % 3] + const currPlayer = [player1, player2, player3][index % 3] + const currPlayerCurrentSource = currentSources[index % 3] + const nextPlayer = [player1, player2, player3][(index + 1) % 3] + const nextPlayerCurrentSource = currentSources[(index + 1) % 3] + + if (prevVideo && prevVideo !== prevPlayerCurrentSource) { + prevPlayer.replace(prevVideo) + currentSources[index + (2 % 3)] = prevVideo + } + prevPlayer.pause() + + if (currVideo) { + if (currVideo !== currPlayerCurrentSource) { + currPlayer.replace(currVideo) + currentSources[index % 3] = currVideo + } + currPlayer.play() + } + + if (nextVideo && nextVideo !== nextPlayerCurrentSource) { + nextPlayer.replace(nextVideo) + currentSources[(index + 1) % 3] = nextVideo + } + nextPlayer.pause() + + return currentSources + }) + }) + const onViewableItemsChanged = useCallback( ({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { if (viewableItems[0] && viewableItems[0].index !== null) { - setCurrentIndex(viewableItems[0].index) + updateVideoState(viewableItems[0].index) } }, - [], + [updateVideoState], ) return ( @@ -170,7 +235,7 @@ function YoloFeed() { }} showsVerticalScrollIndicator={false} onViewableItemsChanged={onViewableItemsChanged} - viewabilityConfig={{itemVisiblePercentThreshold: 75}} + viewabilityConfig={{itemVisiblePercentThreshold: 95}} /> ) } @@ -183,7 +248,6 @@ function VibeItem({ player, post, embed, - active, loaded, }: { player: VideoPlayer @@ -194,42 +258,6 @@ function VibeItem({ }) { const {height, width} = useWindowDimensions() const insets = useSafeAreaInsets() - const source = embed.playlist - const sourceChangeEvent = useEvent(player, 'sourceChange') as { - // incorrect types - source: {uri?: string} - oldSource: {uri?: string} - } - - // for initial video - useEffect will handle the typical case where - // videos have a chance to preload - const maybePlay = useNonReactiveCallback(() => { - if (active && !player.playing) { - player.play() - } - }) - - useEffect(() => { - if (loaded && sourceChangeEvent?.source?.uri !== source) { - player.replace(source) - // play next tick - const timeout = setTimeout(() => { - maybePlay() - }, 0) - return () => { - clearTimeout(timeout) - } - } - }, [sourceChangeEvent?.source?.uri, loaded, source, player, maybePlay]) - - useEffect(() => { - if (active) { - player.play() - } else { - // should be a cleanup function, but that causes a crash - player.pause() - } - }, [active, player]) const screenAspectRatio = (width - insets.left - insets.right) / (height - insets.bottom) @@ -387,10 +415,12 @@ function ExpandableRichTextView({ { - LayoutAnimation.configureNext({ - duration: 500, - update: {type: 'spring', springDamping: 0.6}, - }) + if (contentHeight !== 0) { + LayoutAnimation.configureNext({ + duration: 500, + update: {type: 'spring', springDamping: 0.6}, + }) + } setContentHeight(h) }} style={{height: Math.min(contentHeight, screenHeight * 0.5)}} From 0d8f6fd1162b79fb1459cdca9fdf73a8599fd163 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 16 Jan 2025 00:08:20 +0000 Subject: [PATCH 09/59] use thumbnails, improve perf significantly --- src/screens/Feeds/VibeScreen.tsx | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 4b9be8f3f1..5c5b41d0f1 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -10,6 +10,7 @@ import { import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {runOnJS} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' +import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import { @@ -135,12 +136,11 @@ function YoloFeed() { player={player} post={post} embed={post.embed} - loaded={ + active={ isFocused && - Math.abs(index - currentIndex) < 2 && + index === currentIndex && currentSource === post.embed.playlist } - active={isFocused && index === currentIndex} /> ) }, @@ -248,13 +248,12 @@ function VibeItem({ player, post, embed, - loaded, + active, }: { player: VideoPlayer post: AppBskyFeedDefs.PostView embed: AppBskyEmbedVideo.View active: boolean - loaded: boolean }) { const {height, width} = useWindowDimensions() const insets = useSafeAreaInsets() @@ -276,12 +275,31 @@ function VibeItem({ width, }}> - {loaded && ( + {active && ( + )} + {embed.thumbnail && ( + )} From 5f2dd5015eb1d963ee4f2d0af1280a886d8f0369 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 16 Jan 2025 00:38:16 +0000 Subject: [PATCH 10/59] better android loading --- src/screens/Feeds/VibeScreen.tsx | 139 +++++++++++++++++++------------ 1 file changed, 84 insertions(+), 55 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 5c5b41d0f1..6bc5da071b 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -10,9 +10,15 @@ import { import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {runOnJS} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' +import {useEvent} from 'expo' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' -import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' +import { + useVideoPlayer, + VideoPlayer, + VideoPlayerStatus, + VideoView, +} from 'expo-video' import { AppBskyEmbedVideo, AppBskyFeedDefs, @@ -45,6 +51,7 @@ import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' +import {Loader} from '#/components/Loader' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' @@ -268,6 +275,8 @@ function VibeItem({ // set the video to be cover rather than contain const isCloseEnough = videoAspectRatio < screenAspectRatio * 1.2 + const {status} = useEvent(player, 'statusChange', {status: player.status}) + return ( )} - + ) @@ -311,9 +320,11 @@ function VibeItem({ function VibeOverlay({ player, post, + status, }: { player: VideoPlayer post: AppBskyFeedDefs.PostView + status: VideoPlayerStatus }) { const postShadow = usePostShadow(post) const insets = useSafeAreaInsets() @@ -357,59 +368,77 @@ function VibeOverlay({ }) return ( - - - - - - - - - {sanitizeDisplayName( - post.author.displayName || post.author.handle, - )} - - - {sanitizeHandle(post.author.handle, '@')} - + <> + + + + + + + + + {sanitizeDisplayName( + post.author.displayName || post.author.handle, + )} + + + {sanitizeHandle(post.author.handle, '@')} + + - - {record?.text?.trim() && ( - - )} - {postShadow !== POST_TOMBSTONE && record && ( - - navigation.navigate('PostThread', {name: post.author.did, rkey}) - } - big - /> - )} - - - + {record?.text?.trim() && ( + + )} + {postShadow !== POST_TOMBSTONE && record && ( + + navigation.navigate('PostThread', { + name: post.author.did, + rkey, + }) + } + big + /> + )} + + + + {status === 'loading' && ( + + + + )} + ) } From 69b814d235f84a8d739ae138757c08f521629bdf Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 16 Jan 2025 00:57:44 +0000 Subject: [PATCH 11/59] improve aspect ratio --- src/screens/Feeds/VibeScreen.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 6bc5da071b..349ec7416d 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -265,15 +265,11 @@ function VibeItem({ const {height, width} = useWindowDimensions() const insets = useSafeAreaInsets() - const screenAspectRatio = - (width - insets.left - insets.right) / (height - insets.bottom) - const videoAspectRatio = (embed.aspectRatio?.width ?? 1) / (embed.aspectRatio?.height ?? 1) - // if the video is either taller, on only 20% shorter than the screen, - // set the video to be cover rather than contain - const isCloseEnough = videoAspectRatio < screenAspectRatio * 1.2 + // if the video tall enough (tiktok/reels are 9:16) go cover mode + const isCloseEnough = false && videoAspectRatio <= 9 / 16 const {status} = useEvent(player, 'statusChange', {status: player.status}) @@ -491,10 +487,7 @@ function ExpandableRichTextView({ @@ -412,10 +527,10 @@ function VibeOverlay({ authorHandle={post.author.handle} /> )} - {postShadow !== POST_TOMBSTONE && record && ( + {record && ( From eac4a6753350872921f356a1c094d2d8db61a554 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 16 Jan 2025 14:43:41 +0000 Subject: [PATCH 18/59] remove jank animation --- src/screens/Feeds/VibeScreen.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 96f096f70d..a4f745ed75 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -258,10 +258,12 @@ function YoloFeed() { useFocusEffect( useCallback(() => { if (!players) { + // create players, set sources, start playing updateVideoState() } return () => { if (players) { + // manually release players when offscreen players.forEach(p => p.release()) setPlayers(null) } @@ -365,7 +367,13 @@ function VibeItem({ {backgroundColor: 'rgba(0, 0, 0, 0.8)'}, ]}> + style={[ + a.text_2xl, + a.font_heavy, + a.text_center, + a.leading_tight, + a.mx_xl, + ]}> Post has been deleted @@ -582,15 +590,11 @@ function ExpandableRichTextView({ { - if (contentHeight !== 0) { - LayoutAnimation.configureNext( - h > contentHeight - ? { - duration: 500, - update: {type: 'spring', springDamping: 0.6}, - } - : LayoutAnimation.Presets.easeInEaseOut, - ) + if (expanded) { + LayoutAnimation.configureNext({ + duration: 500, + update: {type: 'spring', springDamping: 0.6}, + }) } setContentHeight(h) }} From 6d242c389181b5a31b2586cf8ceb620e29a055d8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 10:56:20 -0600 Subject: [PATCH 19/59] Add grid --- src/components/VideoPostCard.tsx | 131 +++++++++++++ src/components/feeds/PostFeedVideoGridRow.tsx | 20 ++ src/view/com/posts/PostFeed.tsx | 183 +++++++++++------- 3 files changed, 260 insertions(+), 74 deletions(-) create mode 100644 src/components/VideoPostCard.tsx create mode 100644 src/components/feeds/PostFeedVideoGridRow.tsx diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx new file mode 100644 index 0000000000..70a2a0d608 --- /dev/null +++ b/src/components/VideoPostCard.tsx @@ -0,0 +1,131 @@ +import {Pressable,View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {AppBskyEmbedVideo} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {sanitizeHandle} from '#/lib/strings/handles' +import {FeedPostSliceItem} from '#/state/queries/post-feed' +import {formatCount} from '#/view/com/util/numeric/format' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {BLUE_HUE} from '#/alf/util/colorGeneration' +import {select} from '#/alf/util/themeSelector' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2' +import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' +import {Text} from '#/components/Typography' + +export function VideoPostCard({post}: {post: FeedPostSliceItem}) { + const t = useTheme() + const {_, i18n} = useLingui() + const embed = post.post.embed + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + + if (!AppBskyEmbedVideo.isView(embed)) { + // TODO unavailable? + return null + } + + const {thumbnail} = embed + const black = select(t.name, { + light: t.atoms.bg_contrast_25.backgroundColor, + dark: t.atoms.bg_contrast_25.backgroundColor, + dim: `hsl(${BLUE_HUE}, 28%, 6%)`, + }) + + return ( + + + + + + + + + + + + + + + {formatCount(i18n, post.post.likeCount || 0)} + + + + + + {formatCount(i18n, post.post.repostCount || 0)} + + + + + + + + + + {sanitizeHandle(post.post.author.handle, '@')} + + + + ) +} diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx new file mode 100644 index 0000000000..c82ee9f7c3 --- /dev/null +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -0,0 +1,20 @@ +import {View} from 'react-native' + +import {FeedPostSliceItem} from '#/state/queries/post-feed' +import {atoms as a, useGutters} from '#/alf' +import {VideoPostCard} from '#/components/VideoPostCard' + +export function PostFeedVideoGridRow({posts}: {posts: FeedPostSliceItem[]}) { + const gutters = useGutters(['base', 'base', 0, 'base']) + return ( + + + {posts.map(post => ( + + + + ))} + + + ) +} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index a06230fd68..3bc7a669c0 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -33,6 +33,7 @@ import { FeedDescriptor, FeedParams, FeedPostSlice, + FeedPostSliceItem, pollLatest, RQKEY, usePostFeedQuery, @@ -48,6 +49,7 @@ import { SuggestedFollows, VideoModeEntranceInterstitial, } from '#/components/FeedInterstitials' +import {PostFeedVideoGridRow} from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' @@ -77,7 +79,7 @@ type FeedRow = key: string } | { - type: 'slice' + type: 'slice' // TODO can we remove? key: string slice: FeedPostSlice } @@ -88,6 +90,11 @@ type FeedRow = indexInSlice: number showReplyTo: boolean } + | { + type: 'videoGridRow' + key: string + posts: FeedPostSliceItem[] + } | { type: 'sliceViewFullThread' key: string @@ -175,7 +182,7 @@ let PostFeed = ({ const checkForNewRef = React.useRef<(() => void) | null>(null) const lastFetchRef = React.useRef(Date.now()) const [feedType, feedUri, feedTab] = feed.split('|') - const {gtTablet} = useBreakpoints() + const {gtMobile, gtTablet} = useBreakpoints() const opts = React.useMemo( () => ({enabled, ignoreFilterFor}), @@ -318,88 +325,113 @@ let PostFeed = ({ } else if (data) { let sliceIndex = -1 for (const page of data?.pages) { - for (const slice of page.slices) { - sliceIndex++ + if (feedKind === 'thevids') { + if (sliceIndex === -1) { + arr.push({ + type: 'videoModeEntrance', + key: 'videoModeEntrance', + }) + } - if (hasSession) { - if (feedKind === 'discover') { - if (sliceIndex === 0) { - if (showProgressIntersitial) { - arr.push({ - type: 'interstitialProgressGuide', - key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, - }) - } - if (!gtTablet && !trendingDisabled) { - arr.push({ - type: 'interstitialTrending', - key: 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, - }) - } - } else if (sliceIndex === 30) { - arr.push({ - type: 'interstitialFollows', - key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, - }) - } - } else if (feedKind === 'profile') { - if (sliceIndex === 5) { - arr.push({ - type: 'interstitialFollows', - key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, - }) - } - } else if (feedKind === 'thevids') { - if (sliceIndex === 0) { - arr.push({ - type: 'videoModeEntrance', - key: - 'videoModeEntrance-' + sliceIndex + '-' + lastFetchedAt, - }) - } + const rows: FeedPostSliceItem[][] = [] + for (let i = 0; i < page.slices.length; i++) { + const slice = page.slices[i] + const root = slice.items.at(0) + if (!root) continue + const cols = gtMobile ? 3 : 2 + if (i % cols === 0) { + rows.push([root]) + } else { + rows[rows.length - 1].push(root) } } - if (slice.isIncompleteThread && slice.items.length >= 3) { - const beforeLast = slice.items.length - 2 - const last = slice.items.length - 1 + for (const row of rows) { + sliceIndex++ + arr.push({ - type: 'sliceItem', - key: slice.items[0]._reactKey, - slice: slice, - indexInSlice: 0, - showReplyTo: false, + type: 'videoGridRow', + key: row.map(r => r._reactKey).join('-'), + posts: row, }) - arr.push({ - type: 'sliceViewFullThread', - key: slice._reactKey + '-viewFullThread', - uri: slice.items[0].uri, - }) - arr.push({ - 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({ - type: 'sliceItem', - key: slice.items[last]._reactKey, - slice: slice, - indexInSlice: last, - showReplyTo: false, - }) - } else { - for (let i = 0; i < slice.items.length; i++) { + } + } else { + for (const slice of page.slices) { + sliceIndex++ + + if (hasSession) { + if (feedKind === 'discover') { + if (sliceIndex === 0) { + if (showProgressIntersitial) { + arr.push({ + type: 'interstitialProgressGuide', + key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, + }) + } + if (!gtTablet && !trendingDisabled) { + arr.push({ + type: 'interstitialTrending', + key: + 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, + }) + } + } else if (sliceIndex === 30) { + arr.push({ + type: 'interstitialFollows', + key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, + }) + } + } else if (feedKind === 'profile') { + if (sliceIndex === 5) { + arr.push({ + type: 'interstitialFollows', + key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, + }) + } + } + } + + if (slice.isIncompleteThread && slice.items.length >= 3) { + const beforeLast = slice.items.length - 2 + const last = slice.items.length - 1 arr.push({ type: 'sliceItem', - key: slice.items[i]._reactKey, + key: slice.items[0]._reactKey, slice: slice, - indexInSlice: i, - showReplyTo: i === 0, + indexInSlice: 0, + showReplyTo: false, }) + arr.push({ + type: 'sliceViewFullThread', + key: slice._reactKey + '-viewFullThread', + uri: slice.items[0].uri, + }) + arr.push({ + 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({ + 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({ + type: 'sliceItem', + key: slice.items[i]._reactKey, + slice: slice, + indexInSlice: i, + showReplyTo: i === 0, + }) + } } } } @@ -432,6 +464,7 @@ let PostFeed = ({ showProgressIntersitial, trendingDisabled, gtTablet, + gtMobile, ]) // events @@ -556,6 +589,8 @@ let PostFeed = ({ ) } else if (row.type === 'sliceViewFullThread') { return + } else if (row.type === 'videoGridRow') { + return } else { return null } From cfbb50c11aa967df3a41759a9d550e92ed85b5b2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 16 Jan 2025 17:03:04 +0000 Subject: [PATCH 20/59] improve contract, fix double tap --- src/screens/Feeds/VibeScreen.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index a4f745ed75..02241f8490 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -444,7 +444,6 @@ function VibeOverlay({ const insets = useSafeAreaInsets() const t = useTheme() const navigation = useNavigation() - const [expanded, setExpanded] = useState(false) const {status} = useEvent(player, 'statusChange', {status: player.status}) const doubleTapRef = useRef | null>(null) const [queueLike] = usePostLikeMutationQueue(post, 'Vibe') @@ -454,6 +453,7 @@ function VibeOverlay({ }) const togglePlayPause = () => { + doubleTapRef.current = null if (player.playing) { player.pause() } else { @@ -467,7 +467,7 @@ function VibeOverlay({ doubleTapRef.current = null queueLike() } else { - doubleTapRef.current = setTimeout(togglePlayPause, 300) + doubleTapRef.current = setTimeout(togglePlayPause, 200) } } @@ -506,11 +506,7 @@ function VibeOverlay({ @@ -529,8 +525,6 @@ function VibeOverlay({ {record?.text?.trim() && ( @@ -573,15 +567,12 @@ function VibeOverlay({ function ExpandableRichTextView({ value, authorHandle, - expanded, - setExpanded, }: { value: RichTextAPI authorHandle?: string - expanded: boolean - setExpanded: React.Dispatch> }) { const {height: screenHeight} = useWindowDimensions() + const [expanded, setExpanded] = useState(false) const [constrained, setConstrained] = useState(false) const [contentHeight, setContentHeight] = useState(0) const {_} = useLingui() From a063966fb0e2c121e92f16ad7ab0a02748a1223e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 13:44:43 -0600 Subject: [PATCH 21/59] Filter out posts without videos --- src/components/VideoPostCard.tsx | 30 ++++++++----------- src/components/feeds/PostFeedVideoGridRow.tsx | 15 ++++++++-- src/view/com/posts/PostFeed.tsx | 13 +++++--- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 70a2a0d608..ea1092f760 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -1,12 +1,11 @@ -import {Pressable,View} from 'react-native' +import {Pressable, View} from 'react-native' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' -import {AppBskyEmbedVideo} from '@atproto/api' +import {AppBskyEmbedVideo,AppBskyFeedDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeHandle} from '#/lib/strings/handles' -import {FeedPostSliceItem} from '#/state/queries/post-feed' import {formatCount} from '#/view/com/util/numeric/format' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -17,20 +16,21 @@ import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2 import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' import {Text} from '#/components/Typography' -export function VideoPostCard({post}: {post: FeedPostSliceItem}) { +export function VideoPostCard({post}: {post: AppBskyFeedDefs.PostView}) { const t = useTheme() const {_, i18n} = useLingui() - const embed = post.post.embed + const embed = post.embed const { state: hovered, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() - if (!AppBskyEmbedVideo.isView(embed)) { - // TODO unavailable? - return null - } + /** + * Filtering should be done at a higher level, such as `PostFeed` or + * `PostFeedVideoGridRow`, but we need to protect here as well. + */ + if (!AppBskyEmbedVideo.isView(embed)) return null const {thumbnail} = embed const black = select(t.name, { @@ -95,13 +95,13 @@ export function VideoPostCard({post}: {post: FeedPostSliceItem}) { - {formatCount(i18n, post.post.likeCount || 0)} + {formatCount(i18n, post.likeCount || 0)} - {formatCount(i18n, post.post.repostCount || 0)} + {formatCount(i18n, post.repostCount || 0)} @@ -109,11 +109,7 @@ export function VideoPostCard({post}: {post: FeedPostSliceItem}) { - + - {sanitizeHandle(post.post.author.handle, '@')} + {sanitizeHandle(post.author.handle, '@')} diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index c82ee9f7c3..2d1a035632 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -1,16 +1,27 @@ import {View} from 'react-native' +import {AppBskyEmbedVideo} from '@atproto/api' import {FeedPostSliceItem} from '#/state/queries/post-feed' import {atoms as a, useGutters} from '#/alf' import {VideoPostCard} from '#/components/VideoPostCard' -export function PostFeedVideoGridRow({posts}: {posts: FeedPostSliceItem[]}) { +export function PostFeedVideoGridRow({slices}: {slices: FeedPostSliceItem[]}) { const gutters = useGutters(['base', 'base', 0, 'base']) + const posts = slices + .filter(slice => AppBskyEmbedVideo.isView(slice.post.embed)) + .map(slice => slice.post) + + /** + * This should not happen because we should be filtering out posts without + * videos within the `PostFeed` component. + */ + if (posts.length !== slices.length) return null + return ( {posts.map(post => ( - + ))} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 3bc7a669c0..812e7d56a3 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -9,7 +9,7 @@ import { View, ViewStyle, } from 'react-native' -import {AppBskyActorDefs} from '@atproto/api' +import {AppBskyActorDefs, AppBskyEmbedVideo} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' @@ -93,7 +93,7 @@ type FeedRow = | { type: 'videoGridRow' key: string - posts: FeedPostSliceItem[] + slices: FeedPostSliceItem[] } | { type: 'sliceViewFullThread' @@ -338,6 +338,11 @@ let PostFeed = ({ const slice = page.slices[i] const root = slice.items.at(0) if (!root) continue + // TODO test this + if (!AppBskyEmbedVideo.isView(root.post.embed)) { + i-- + continue + } const cols = gtMobile ? 3 : 2 if (i % cols === 0) { rows.push([root]) @@ -352,7 +357,7 @@ let PostFeed = ({ arr.push({ type: 'videoGridRow', key: row.map(r => r._reactKey).join('-'), - posts: row, + slices: row, }) } } else { @@ -590,7 +595,7 @@ let PostFeed = ({ } else if (row.type === 'sliceViewFullThread') { return } else if (row.type === 'videoGridRow') { - return + return } else { return null } From de273f70b48a138d677541108efe3bfd3cfb60ef Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 13:57:08 -0600 Subject: [PATCH 22/59] Only do grid on native --- src/view/com/posts/PostFeed.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 812e7d56a3..70c3dff069 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -24,7 +24,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig' import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' -import {isIOS, isWeb} from '#/platform/detection' +import {isIOS, isNative,isWeb} from '#/platform/detection' import {listenPostCreated} from '#/state/events' import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useTrendingSettings} from '#/state/preferences/trending' @@ -325,7 +325,7 @@ let PostFeed = ({ } else if (data) { let sliceIndex = -1 for (const page of data?.pages) { - if (feedKind === 'thevids') { + if (feedKind === 'thevids' && isNative) { if (sliceIndex === -1) { arr.push({ type: 'videoModeEntrance', From 3e217c861acb7cc4987a6e8cc9cbc93b4ee0eb85 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 15:24:08 -0600 Subject: [PATCH 23/59] Pipe through feedSourceUri and link to feed --- src/components/VideoPostCard.tsx | 37 +++++++++++++++---- src/components/feeds/PostFeedVideoGridRow.tsx | 10 ++++- src/lib/routes/types.ts | 2 +- src/view/com/posts/PostFeed.tsx | 11 +++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index ea1092f760..9a49add5b2 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -1,10 +1,11 @@ -import {Pressable, View} from 'react-native' +import {View} from 'react-native' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' -import {AppBskyEmbedVideo,AppBskyFeedDefs} from '@atproto/api' +import {AppBskyEmbedVideo, AppBskyFeedDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {VIBES_FEED_URI} from '#/lib/constants' import {sanitizeHandle} from '#/lib/strings/handles' import {formatCount} from '#/view/com/util/numeric/format' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -14,9 +15,16 @@ import {select} from '#/alf/util/themeSelector' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2' import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' +import {Link} from '#/components/Link' import {Text} from '#/components/Typography' -export function VideoPostCard({post}: {post: AppBskyFeedDefs.PostView}) { +export function VideoPostCard({ + post, + sourceFeedUri = VIBES_FEED_URI, +}: { + post: AppBskyFeedDefs.PostView + sourceFeedUri?: string +}) { const t = useTheme() const {_, i18n} = useLingui() const embed = post.embed @@ -40,11 +48,24 @@ export function VideoPostCard({post}: {post: AppBskyFeedDefs.PostView}) { }) return ( - + onHoverOut={onHoverOut} + style={[ + a.flex_col, + { + alignItems: undefined, + justifyContent: undefined, + }, + ]}> - + ) } diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index 2d1a035632..11fa84eb73 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -5,7 +5,13 @@ import {FeedPostSliceItem} from '#/state/queries/post-feed' import {atoms as a, useGutters} from '#/alf' import {VideoPostCard} from '#/components/VideoPostCard' -export function PostFeedVideoGridRow({slices}: {slices: FeedPostSliceItem[]}) { +export function PostFeedVideoGridRow({ + slices, + sourceFeedUri, +}: { + slices: FeedPostSliceItem[] + sourceFeedUri: string +}) { const gutters = useGutters(['base', 'base', 0, 'base']) const posts = slices .filter(slice => AppBskyEmbedVideo.isView(slice.post.embed)) @@ -22,7 +28,7 @@ export function PostFeedVideoGridRow({slices}: {slices: FeedPostSliceItem[]}) { {posts.map(post => ( - + ))} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 744a5a0d85..ab23a331c1 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -57,7 +57,7 @@ export type CommonNavigatorParams = { StarterPackShort: {code: string} StarterPackWizard: undefined StarterPackEdit: {rkey?: string} - TempVibe: undefined + TempVibe: {feedUri: string; cursor?: string} } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 70c3dff069..7844fb40b8 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -24,7 +24,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig' import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' -import {isIOS, isNative,isWeb} from '#/platform/detection' +import {isIOS, isNative, isWeb} from '#/platform/detection' import {listenPostCreated} from '#/state/events' import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useTrendingSettings} from '#/state/preferences/trending' @@ -94,6 +94,7 @@ type FeedRow = type: 'videoGridRow' key: string slices: FeedPostSliceItem[] + sourceFeedUri: string } | { type: 'sliceViewFullThread' @@ -358,6 +359,7 @@ let PostFeed = ({ type: 'videoGridRow', key: row.map(r => r._reactKey).join('-'), slices: row, + sourceFeedUri: feedUri, }) } } else { @@ -595,7 +597,12 @@ let PostFeed = ({ } else if (row.type === 'sliceViewFullThread') { return } else if (row.type === 'videoGridRow') { - return + return ( + + ) } else { return null } From 50fccd8347d795de28433730dc73279885937553 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 15:24:49 -0600 Subject: [PATCH 24/59] Handle passed through params --- src/screens/Feeds/VibeScreen.tsx | 9 ++++++-- src/state/queries/post-feed.ts | 37 ++++++++++++++++---------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 02241f8490..e6c0170c34 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -24,13 +24,15 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import { + RouteProp, useFocusEffect, useIsFocused, useNavigation, + useRoute, } from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {HITSLOP_20, VIBES_FEED_URI} from '#/lib/constants' +import {HITSLOP_20} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' @@ -107,6 +109,7 @@ export function VibeScreen({}: Props) { } function YoloFeed() { + const {params} = useRoute>() const isFocused = useIsFocused() const { data, @@ -115,7 +118,9 @@ function YoloFeed() { hasNextPage, isFetchingNextPage, fetchNextPage, - } = usePostFeedQuery(`feedgen|${VIBES_FEED_URI}`) + } = usePostFeedQuery(`feedgen|${params.feedUri}`, { + initialCursor: params.cursor, + }) const videos = data?.pages.flatMap(page => page.slices.flatMap(slice => slice.items), diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 2eb604627e..35062ebef9 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -61,9 +61,10 @@ export type FeedDescriptor = export interface FeedParams { mergeFeedEnabled?: boolean mergeFeedSources?: string[] + initialCursor?: string } -type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined +type RQPageParam = {cursor: string | undefined; api?: FeedAPI} | undefined export const RQKEY_ROOT = 'post-feed' export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { @@ -171,24 +172,24 @@ export function usePostFeedQuery( queryKey: RQKEY(feedDesc, params), async queryFn({pageParam}: {pageParam: RQPageParam}) { logger.debug('usePostFeedQuery', {feedDesc, cursor: pageParam?.cursor}) - const {api, cursor} = pageParam - ? pageParam - : { - api: createApi({ - feedDesc, - feedParams: params || {}, - feedTuners, - agent, - // Not in the query key because they don't change: - userInterests, - // Not in the query key. Reacting to it switching isn't important: - enableFollowingToDiscoverFallback, - }), - cursor: undefined, - } + console.log('PAGE', feedDesc, pageParam?.cursor) + const cursor = pageParam?.cursor + let api = pageParam?.api + if (!api) { + api = createApi({ + feedDesc, + feedParams: params || {}, + feedTuners, + agent, + // Not in the query key because they don't change: + userInterests, + // Not in the query key. Reacting to it switching isn't important: + enableFollowingToDiscoverFallback, + }) + } try { - const res = await api.fetch({cursor, limit: fetchLimit}) + const res = await api!.fetch({cursor, limit: fetchLimit}) /* * If this is a public view, we need to check if posts fail moderation. @@ -223,7 +224,7 @@ export function usePostFeedQuery( throw e } }, - initialPageParam: undefined, + initialPageParam: {api: undefined, cursor: params?.initialCursor}, getNextPageParam: lastPage => lastPage.cursor ? { From 786155ce5b6b4b31442b7b74b126cde380aa7825 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 15:31:42 -0600 Subject: [PATCH 25/59] Partial revert, just filter posts to start at index --- src/components/VideoPostCard.tsx | 2 +- src/lib/routes/types.ts | 2 +- src/screens/Feeds/VibeScreen.tsx | 12 +++++++---- src/state/queries/post-feed.ts | 37 ++++++++++++++++---------------- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 9a49add5b2..401771dd1b 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -54,7 +54,7 @@ export function VideoPostCard({ screen: 'TempVibe', params: { feedUri: sourceFeedUri, - cursor: post.cid, + postUri: post.uri, }, }} onHoverIn={onHoverIn} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index ab23a331c1..85bfc923af 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -57,7 +57,7 @@ export type CommonNavigatorParams = { StarterPackShort: {code: string} StarterPackWizard: undefined StarterPackEdit: {rkey?: string} - TempVibe: {feedUri: string; cursor?: string} + TempVibe: {feedUri: string; postUri?: string} } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index e6c0170c34..050a6e0dab 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -118,13 +118,17 @@ function YoloFeed() { hasNextPage, isFetchingNextPage, fetchNextPage, - } = usePostFeedQuery(`feedgen|${params.feedUri}`, { - initialCursor: params.cursor, - }) + } = usePostFeedQuery(`feedgen|${params.feedUri}`) - const videos = data?.pages.flatMap(page => + let videos = data?.pages.flatMap(page => page.slices.flatMap(slice => slice.items), ) + const startingVideoIndex = videos?.findIndex(video => { + return video.post.uri === params.postUri + }) + if (videos && startingVideoIndex && startingVideoIndex > -1) { + videos = videos.slice(startingVideoIndex) + } const [currentSources, setCurrentSources] = useState< [string | null, string | null, string | null] diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 35062ebef9..2eb604627e 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -61,10 +61,9 @@ export type FeedDescriptor = export interface FeedParams { mergeFeedEnabled?: boolean mergeFeedSources?: string[] - initialCursor?: string } -type RQPageParam = {cursor: string | undefined; api?: FeedAPI} | undefined +type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined export const RQKEY_ROOT = 'post-feed' export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { @@ -172,24 +171,24 @@ export function usePostFeedQuery( queryKey: RQKEY(feedDesc, params), async queryFn({pageParam}: {pageParam: RQPageParam}) { logger.debug('usePostFeedQuery', {feedDesc, cursor: pageParam?.cursor}) - console.log('PAGE', feedDesc, pageParam?.cursor) - const cursor = pageParam?.cursor - let api = pageParam?.api - if (!api) { - api = createApi({ - feedDesc, - feedParams: params || {}, - feedTuners, - agent, - // Not in the query key because they don't change: - userInterests, - // Not in the query key. Reacting to it switching isn't important: - enableFollowingToDiscoverFallback, - }) - } + const {api, cursor} = pageParam + ? pageParam + : { + api: createApi({ + feedDesc, + feedParams: params || {}, + feedTuners, + agent, + // Not in the query key because they don't change: + userInterests, + // Not in the query key. Reacting to it switching isn't important: + enableFollowingToDiscoverFallback, + }), + cursor: undefined, + } try { - const res = await api!.fetch({cursor, limit: fetchLimit}) + const res = await api.fetch({cursor, limit: fetchLimit}) /* * If this is a public view, we need to check if posts fail moderation. @@ -224,7 +223,7 @@ export function usePostFeedQuery( throw e } }, - initialPageParam: {api: undefined, cursor: params?.initialCursor}, + initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor ? { From 7cdb2789fd4353fdc1867b1c10ce236a69bf5acd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 16:39:33 -0600 Subject: [PATCH 26/59] Clean up cards, remove entry interstitial --- src/components/FeedInterstitials.tsx | 46 +------- src/components/VideoPostCard.tsx | 100 +++++++++--------- src/components/feeds/PostFeedVideoGridRow.tsx | 2 +- src/view/com/posts/PostFeed.tsx | 19 +--- 4 files changed, 56 insertions(+), 111 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 9f1505cc98..268a5ff5b8 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -18,16 +18,14 @@ import * as userActionHistory from '#/state/userActionHistory' import {SeenPost} from '#/state/userActionHistory' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' -import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {InlineLinkText, Link} from '#/components/Link' +import {InlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' -import {LinearGradientBackground} from './LinearGradientBackground' import {ProgressGuideList} from './ProgressGuide/List' const MOBILE_CARD_WIDTH = 300 @@ -520,43 +518,3 @@ export function ProgressGuide() { ) } - -export function VideoModeEntranceInterstitial() { - const {_} = useLingui() - const t = useTheme() - - return ( - - - - - NEW! - {' '} - Scroll this feed in video mode - - - - Start watching - - - - - - ) -} diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 401771dd1b..c8072464df 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -1,7 +1,7 @@ import {View} from 'react-native' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' -import {AppBskyEmbedVideo, AppBskyFeedDefs} from '@atproto/api' +import {AppBskyEmbedVideo, AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -16,6 +16,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState' import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2' import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' import {Link} from '#/components/Link' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' export function VideoPostCard({ @@ -29,9 +30,9 @@ export function VideoPostCard({ const {_, i18n} = useLingui() const embed = post.embed const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, + state: pressed, + onIn: onPressIn, + onOut: onPressOut, } = useInteractionState() /** @@ -40,9 +41,12 @@ export function VideoPostCard({ */ if (!AppBskyEmbedVideo.isView(embed)) return null + const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : '' + const likeCount = post?.likeCount ?? 0 + const repostCount = post?.repostCount ?? 0 const {thumbnail} = embed const black = select(t.name, { - light: t.atoms.bg_contrast_25.backgroundColor, + light: t.palette.black, dark: t.atoms.bg_contrast_25.backgroundColor, dim: `hsl(${BLUE_HUE}, 28%, 6%)`, }) @@ -57,8 +61,8 @@ export function VideoPostCard({ postUri: post.uri, }, }} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut} + onPressIn={onPressIn} + onPressOut={onPressOut} style={[ a.flex_col, { @@ -69,7 +73,7 @@ export function VideoPostCard({ + - - - - - - {formatCount(i18n, post.likeCount || 0)} - - - - - - {formatCount(i18n, post.repostCount || 0)} - - + {likeCount > 0 && ( + + + + {formatCount(i18n, likeCount)} + + + )} + {repostCount > 0 && ( + + + + {formatCount(i18n, repostCount)} + + + )} - - - - {sanitizeHandle(post.author.handle, '@')} - + + {text && ( + + {text} + + )} + + + + {sanitizeHandle(post.author.handle, '@')} + + ) diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index 11fa84eb73..222290377b 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -25,7 +25,7 @@ export function PostFeedVideoGridRow({ return ( - + {posts.map(post => ( diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 7844fb40b8..8325756f7c 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -44,11 +44,7 @@ import {List, ListRef} from '#/view/com/util/List' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {useBreakpoints} from '#/alf' -import { - ProgressGuide, - SuggestedFollows, - VideoModeEntranceInterstitial, -} from '#/components/FeedInterstitials' +import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' import {PostFeedVideoGridRow} from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' @@ -113,10 +109,6 @@ type FeedRow = type: 'interstitialTrending' key: string } - | { - type: 'videoModeEntrance' - key: string - } export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null { if (feedRow.type === 'sliceItem') { @@ -327,13 +319,6 @@ let PostFeed = ({ let sliceIndex = -1 for (const page of data?.pages) { if (feedKind === 'thevids' && isNative) { - if (sliceIndex === -1) { - arr.push({ - type: 'videoModeEntrance', - key: 'videoModeEntrance', - }) - } - const rows: FeedPostSliceItem[][] = [] for (let i = 0; i < page.slices.length; i++) { const slice = page.slices[i] @@ -560,8 +545,6 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return - } else if (row.type === 'videoModeEntrance') { - return } else if (row.type === 'sliceItem') { const slice = row.slice if (slice.isFallbackMarker) { From 6a24db43b1b3c666054c903788cbd16f473fef52 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 16:43:00 -0600 Subject: [PATCH 27/59] Tweak handle --- src/alf/themes.ts | 2 +- src/components/VideoPostCard.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/alf/themes.ts b/src/alf/themes.ts index cb97a7065b..82b2e1b405 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -497,7 +497,7 @@ export function createThemes({ color: dimPalette.contrast_400, }, text_contrast_medium: { - color: dimPalette.contrast_700, + color: dimPalette.contrast_600, }, text_contrast_high: { color: dimPalette.contrast_900, diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index c8072464df..f80809fd57 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -138,7 +138,6 @@ export function VideoPostCard({ style={[ a.flex_1, a.text_sm, - a.font_bold, a.leading_tight, t.atoms.text_contrast_medium, ]} From 49d818a875091b6d7025cdc6fb2567c69c4d4364 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 17:09:03 -0600 Subject: [PATCH 28/59] Change constant name --- src/components/VideoPostCard.tsx | 4 ++-- src/lib/constants.ts | 2 +- src/view/com/posts/PostFeed.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index f80809fd57..31c8070bb0 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -5,7 +5,7 @@ import {AppBskyEmbedVideo, AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {VIBES_FEED_URI} from '#/lib/constants' +import {VIDEO_FEED_URI} from '#/lib/constants' import {sanitizeHandle} from '#/lib/strings/handles' import {formatCount} from '#/view/com/util/numeric/format' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -21,7 +21,7 @@ import {Text} from '#/components/Typography' export function VideoPostCard({ post, - sourceFeedUri = VIBES_FEED_URI, + sourceFeedUri = VIDEO_FEED_URI, }: { post: AppBskyFeedDefs.PostView sourceFeedUri?: string diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 145e915c9a..bc099dab98 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -124,7 +124,7 @@ export const BSKY_FEED_OWNER_DIDS = [ export const DISCOVER_FEED_URI = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' -export const VIBES_FEED_URI = +export const VIDEO_FEED_URI = 'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids' export const DISCOVER_SAVED_FEED = { type: 'feed', diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 8325756f7c..0ac1aa1a45 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -17,7 +17,7 @@ import {useQueryClient} from '@tanstack/react-query' import { DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS, - VIBES_FEED_URI, + VIDEO_FEED_URI, } from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' @@ -287,7 +287,7 @@ let PostFeed = ({ feedKind = 'following' } else if (feedUri === DISCOVER_FEED_URI) { feedKind = 'discover' - } else if (feedUri === VIBES_FEED_URI) { + } else if (feedUri === VIDEO_FEED_URI) { feedKind = 'thevids' } else if ( feedType === 'author' && From 340fb007d20818c4443f62221c6e0aafe93544fc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 17:13:41 -0600 Subject: [PATCH 29/59] Rename some things --- src/Navigation.tsx | 4 ++-- src/components/VideoPostCard.tsx | 2 +- src/lib/routes/types.ts | 2 +- src/routes.ts | 3 +-- src/screens/Feeds/VibeScreen.tsx | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index acb8d7fc46..378c4ff76b 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -424,10 +424,10 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { options={{title: title(msg`Edit your starter pack`), requireAuth: true}} /> VibeScreen} options={{ - title: title(msg`Vibe`), + title: title(msg`Video Feed`), requireAuth: true, }} /> diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 31c8070bb0..c30eea3e69 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -55,7 +55,7 @@ export function VideoPostCard({ +type Props = NativeStackScreenProps export function VibeScreen({}: Props) { const {top} = useSafeAreaInsets() @@ -109,7 +109,7 @@ export function VibeScreen({}: Props) { } function YoloFeed() { - const {params} = useRoute>() + const {params} = useRoute>() const isFocused = useIsFocused() const { data, From 0608412a6d2d6fd407a7ddedb41cc18ddcacbaa2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 17:36:59 -0600 Subject: [PATCH 30/59] Make types legit --- src/components/VideoPostCard.tsx | 22 ++++++++++++++----- src/components/feeds/PostFeedVideoGridRow.tsx | 8 +++---- src/lib/routes/types.ts | 4 +++- src/screens/Feeds/VibeScreen.tsx | 14 ++++++++++-- src/state/queries/post-feed.ts | 2 +- src/view/com/posts/PostFeed.tsx | 5 ++++- 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index c30eea3e69..e5898226a4 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -5,8 +5,8 @@ import {AppBskyEmbedVideo, AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {VIDEO_FEED_URI} from '#/lib/constants' import {sanitizeHandle} from '#/lib/strings/handles' +import {AuthorFilter} from '#/state/queries/post-feed' import {formatCount} from '#/view/com/util/numeric/format' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -19,12 +19,24 @@ import {Link} from '#/components/Link' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' +/** + * Kind of like `FeedDescriptor` but not + */ +export type SourceContext = + | {type: 'feedgen'; uri: string; initialPostUri?: string} + | { + type: 'author' + did: string + filter: AuthorFilter + initialPostUri?: string + } + export function VideoPostCard({ post, - sourceFeedUri = VIDEO_FEED_URI, + sourceContext, }: { post: AppBskyFeedDefs.PostView - sourceFeedUri?: string + sourceContext: SourceContext }) { const t = useTheme() const {_, i18n} = useLingui() @@ -57,8 +69,8 @@ export function VideoPostCard({ to={{ screen: 'VideoFeed', params: { - feedUri: sourceFeedUri, - postUri: post.uri, + ...sourceContext, + initialPostUri: post.uri, }, }} onPressIn={onPressIn} diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index 222290377b..60372bef48 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -3,14 +3,14 @@ import {AppBskyEmbedVideo} from '@atproto/api' import {FeedPostSliceItem} from '#/state/queries/post-feed' import {atoms as a, useGutters} from '#/alf' -import {VideoPostCard} from '#/components/VideoPostCard' +import {SourceContext,VideoPostCard} from '#/components/VideoPostCard' export function PostFeedVideoGridRow({ slices, - sourceFeedUri, + sourceContext, }: { slices: FeedPostSliceItem[] - sourceFeedUri: string + sourceContext: SourceContext }) { const gutters = useGutters(['base', 'base', 0, 'base']) const posts = slices @@ -28,7 +28,7 @@ export function PostFeedVideoGridRow({ {posts.map(post => ( - + ))} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index a9570a9dbd..954eb30bef 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -1,6 +1,8 @@ import {NavigationState, PartialState} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +import {SourceContext as VideoFeedSourceContext} from '#/components/VideoPostCard' + export type {NativeStackScreenProps} from '@react-navigation/native-stack' export type CommonNavigatorParams = { @@ -57,7 +59,7 @@ export type CommonNavigatorParams = { StarterPackShort: {code: string} StarterPackWizard: undefined StarterPackEdit: {rkey?: string} - VideoFeed: {feedUri: string; postUri?: string} + VideoFeed: VideoFeedSourceContext } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 41f3718645..579fe9120e 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -111,6 +111,16 @@ export function VibeScreen({}: Props) { function YoloFeed() { const {params} = useRoute>() const isFocused = useIsFocused() + const feedDesc = useMemo(() => { + switch (params.type) { + case 'feedgen': + return `feedgen|${params.uri}` as const + case 'author': + return `author|${params.did}|${params.filter}` as const + default: + throw new Error(`Invalid video feed params ${JSON.stringify(params)}`) + } + }, [params]) const { data, isFetching, @@ -118,13 +128,13 @@ function YoloFeed() { hasNextPage, isFetchingNextPage, fetchNextPage, - } = usePostFeedQuery(`feedgen|${params.feedUri}`) + } = usePostFeedQuery(feedDesc) let videos = data?.pages.flatMap(page => page.slices.flatMap(slice => slice.items), ) const startingVideoIndex = videos?.findIndex(video => { - return video.post.uri === params.postUri + return video.post.uri === params.initialPostUri }) if (videos && startingVideoIndex && startingVideoIndex > -1) { videos = videos.slice(startingVideoIndex) diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 2eb604627e..61dabf5f10 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -44,7 +44,7 @@ import { } from './util' type ActorDid = string -type AuthorFilter = +export type AuthorFilter = | 'posts_with_replies' | 'posts_no_replies' | 'posts_and_author_threads' diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 0ac1aa1a45..32cdea84e2 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -583,7 +583,10 @@ let PostFeed = ({ return ( ) } else { From b61c3d42be036b0f985dbc5b29963780d78e23b4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 17:44:22 -0600 Subject: [PATCH 31/59] Clean up more naming --- src/Navigation.tsx | 4 ++-- src/lib/statsig/events.ts | 8 +++---- src/screens/Feeds/VibeScreen.tsx | 26 ++++++++++++---------- src/view/com/util/post-ctrls/PostCtrls.tsx | 2 +- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 378c4ff76b..d637c4efe5 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -89,7 +89,7 @@ import {Wizard} from '#/screens/StarterPack/Wizard' import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' -import {VibeScreen} from './screens/Feeds/VibeScreen' +import {VideoFeed} from './screens/Feeds/VibeScreen' import {AboutSettingsScreen} from './screens/Settings/AboutSettings' import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings' @@ -425,7 +425,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> VibeScreen} + getComponent={() => VideoFeed} options={{ title: title(msg`Video Feed`), requireAuth: true, diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 220cbd7810..f914ef01f9 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -131,16 +131,16 @@ export type LogEvents = { doesPosterFollowLiker: boolean | undefined likerClout: number | undefined postClout: number | undefined - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' } 'post:repost': { - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' } 'post:unlike': { - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' } 'post:unrepost': { - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' } 'post:mute': {} 'post:unmute': {} diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 579fe9120e..3ff9363bb5 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -66,8 +66,10 @@ function createThreeVideoPlayers( return [p1, p2, p3] } -type Props = NativeStackScreenProps -export function VibeScreen({}: Props) { +export function VideoFeed({}: NativeStackScreenProps< + CommonNavigatorParams, + 'VideoFeed' +>) { const {top} = useSafeAreaInsets() const setMinShellMode = useSetMinimalShellMode() @@ -96,19 +98,19 @@ export function VibeScreen({}: Props) { {/* TODO: needs to be feed name */} - Vibes (wip) + Videos - + ) } -function YoloFeed() { +function Inner() { const {params} = useRoute>() const isFocused = useIsFocused() const feedDesc = useMemo(() => { @@ -161,7 +163,7 @@ function YoloFeed() { const currentSource = currentSources[index % 3] return ( - {player ? ( - + ) : ( embed.thumbnail && ( + player && ) : ( () const {status} = useEvent(player, 'statusChange', {status: player.status}) const doubleTapRef = useRef | null>(null) - const [queueLike] = usePostLikeMutationQueue(post, 'Vibe') + const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index e51353fd31..f73cede352 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -69,7 +69,7 @@ let PostCtrls = ({ style?: StyleProp onPressReply: () => void onPostReply?: (postUri: string | undefined) => void - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'Vibe' + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' threadgateRecord?: AppBskyFeedThreadgate.Record }): React.ReactNode => { const t = useTheme() From 41d3ac495e0dfad3eb837b9ba256284b384b4dd9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 18:17:20 -0600 Subject: [PATCH 32/59] Add placeholder for grid view --- src/components/VideoPostCard.tsx | 70 +++++++++++++++++++ src/components/feeds/PostFeedVideoGridRow.tsx | 18 ++++- src/view/com/posts/PostFeed.tsx | 38 +++++++--- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index e5898226a4..97633dc13e 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -161,3 +161,73 @@ export function VideoPostCard({ ) } + +export function VideoPostCardPlaceholder() { + const t = useTheme() + const black = select(t.name, { + light: t.palette.black, + dark: t.atoms.bg_contrast_25.backgroundColor, + dim: `hsl(${BLUE_HUE}, 28%, 6%)`, + }) + + return ( + + + + + + + + + + + + + + ) +} diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index 60372bef48..d9e2833dfa 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -3,7 +3,11 @@ import {AppBskyEmbedVideo} from '@atproto/api' import {FeedPostSliceItem} from '#/state/queries/post-feed' import {atoms as a, useGutters} from '#/alf' -import {SourceContext,VideoPostCard} from '#/components/VideoPostCard' +import { + SourceContext, + VideoPostCard, + VideoPostCardPlaceholder, +} from '#/components/VideoPostCard' export function PostFeedVideoGridRow({ slices, @@ -35,3 +39,15 @@ export function PostFeedVideoGridRow({ ) } + +export function PostFeedVideoGridRowPlaceholder() { + const gutters = useGutters(['base', 'base', 0, 'base']) + return ( + + + + + + + ) +} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 32cdea84e2..8bb9d7162c 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -45,7 +45,10 @@ import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {useBreakpoints} from '#/alf' import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' -import {PostFeedVideoGridRow} from '#/components/feeds/PostFeedVideoGridRow' +import { + PostFeedVideoGridRow, + PostFeedVideoGridRowPlaceholder, +} from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' @@ -86,6 +89,10 @@ type FeedRow = indexInSlice: number showReplyTo: boolean } + | { + type: 'videoGridRowPlaceholder' + key: string + } | { type: 'videoGridRow' key: string @@ -176,6 +183,7 @@ let PostFeed = ({ const lastFetchRef = React.useRef(Date.now()) const [feedType, feedUri, feedTab] = feed.split('|') const {gtMobile, gtTablet} = useBreakpoints() + const isVideoFeed = feedUri === VIDEO_FEED_URI const opts = React.useMemo( () => ({enabled, ignoreFilterFor}), @@ -287,8 +295,6 @@ let PostFeed = ({ feedKind = 'following' } else if (feedUri === DISCOVER_FEED_URI) { feedKind = 'discover' - } else if (feedUri === VIDEO_FEED_URI) { - feedKind = 'thevids' } else if ( feedType === 'author' && (feedTab === 'posts_and_author_threads' || @@ -318,7 +324,7 @@ let PostFeed = ({ } else if (data) { let sliceIndex = -1 for (const page of data?.pages) { - if (feedKind === 'thevids' && isNative) { + if (isVideoFeed && isNative) { const rows: FeedPostSliceItem[][] = [] for (let i = 0; i < page.slices.length; i++) { const slice = page.slices[i] @@ -436,10 +442,17 @@ let PostFeed = ({ }) } } else { - arr.push({ - type: 'loading', - key: 'loading', - }) + if (isVideoFeed) { + arr.push({ + type: 'videoGridRowPlaceholder', + key: 'videoGridRowPlaceholder', + }) + } else { + arr.push({ + type: 'loading', + key: 'loading', + }) + } } return arr @@ -457,6 +470,7 @@ let PostFeed = ({ trendingDisabled, gtTablet, gtMobile, + isVideoFeed, ]) // events @@ -579,6 +593,14 @@ let PostFeed = ({ ) } else if (row.type === 'sliceViewFullThread') { return + } else if (row.type === 'videoGridRowPlaceholder') { + return ( + + + + + + ) } else if (row.type === 'videoGridRow') { return ( Date: Thu, 16 Jan 2025 18:30:13 -0600 Subject: [PATCH 33/59] Handle web, set up new organization --- src/Navigation.tsx | 2 +- src/screens/VideoFeed/index.tsx | 1 + src/screens/VideoFeed/index.web.tsx | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/screens/VideoFeed/index.tsx create mode 100644 src/screens/VideoFeed/index.web.tsx diff --git a/src/Navigation.tsx b/src/Navigation.tsx index d637c4efe5..a6332c5d88 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -86,10 +86,10 @@ import { StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen' import {Wizard} from '#/screens/StarterPack/Wizard' +import {VideoFeed} from '#/screens/VideoFeed' import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' -import {VideoFeed} from './screens/Feeds/VibeScreen' import {AboutSettingsScreen} from './screens/Settings/AboutSettings' import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings' diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx new file mode 100644 index 0000000000..b9412343e4 --- /dev/null +++ b/src/screens/VideoFeed/index.tsx @@ -0,0 +1 @@ +export {VideoFeed} from '#/screens/Feeds/VibeScreen' diff --git a/src/screens/VideoFeed/index.web.tsx b/src/screens/VideoFeed/index.web.tsx new file mode 100644 index 0000000000..38ec8cc0ad --- /dev/null +++ b/src/screens/VideoFeed/index.web.tsx @@ -0,0 +1,3 @@ +export function VideoScreen() { + return null +} From 118e0318fe1f36dcf52efa65ca54ffe5cdaa9c21 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 18:56:41 -0600 Subject: [PATCH 34/59] Begin work on Header --- src/screens/Feeds/VibeScreen.tsx | 13 +--- src/screens/VideoFeed/Header.tsx | 126 +++++++++++++++++++++++++++++++ src/screens/VideoFeed/types.ts | 13 ++++ 3 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 src/screens/VideoFeed/Header.tsx create mode 100644 src/screens/VideoFeed/types.ts diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 3ff9363bb5..333d60596d 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -46,6 +46,7 @@ import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {Header} from '#/screens/VideoFeed/Header' import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' @@ -71,6 +72,7 @@ export function VideoFeed({}: NativeStackScreenProps< 'VideoFeed' >) { const {top} = useSafeAreaInsets() + const {params} = useRoute>() const setMinShellMode = useSetMinimalShellMode() useFocusEffect( @@ -93,16 +95,7 @@ export function VideoFeed({}: NativeStackScreenProps< a.z_30, {top: 0, left: 0, right: 0, paddingTop: top}, ]}> - - - - - {/* TODO: needs to be feed name */} - Videos - - - - +
diff --git a/src/screens/VideoFeed/Header.tsx b/src/screens/VideoFeed/Header.tsx new file mode 100644 index 0000000000..b40f9865b6 --- /dev/null +++ b/src/screens/VideoFeed/Header.tsx @@ -0,0 +1,126 @@ +import {View} from 'react-native' + +import {sanitizeHandle} from '#/lib/strings/handles' +import {useFeedSourceInfoQuery} from '#/state/queries/feed' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' +import {atoms as a, useBreakpoints} from '#/alf' +import * as Layout from '#/components/Layout' +import {Text} from '#/components/Typography' + +export function HeaderPlaceholder() { + return ( + + + + + + + + + ) +} + +export function Header({ + sourceContext, +}: { + sourceContext: VideoFeedSourceContext +}) { + let content = null + switch (sourceContext.type) { + case 'feedgen': { + content = + break + } + case 'author': + // TODO + default: { + break + } + } + + return ( + + + {content} + + ) +} + +export function FeedHeader({ + sourceContext, +}: { + sourceContext: Exclude +}) { + const {gtMobile} = useBreakpoints() + + const { + data: info, + isLoading, + error, + } = useFeedSourceInfoQuery({uri: sourceContext.uri}) + + if (isLoading) { + return + } else if (error || !info) { + return null + } + + return ( + + {info.avatar && } + + + + {info.displayName} + + + + {sanitizeHandle(info.creatorHandle, '@')} + + + + + ) +} diff --git a/src/screens/VideoFeed/types.ts b/src/screens/VideoFeed/types.ts new file mode 100644 index 0000000000..22723bcc29 --- /dev/null +++ b/src/screens/VideoFeed/types.ts @@ -0,0 +1,13 @@ +import {AuthorFilter} from '#/state/queries/post-feed' + +/** + * Kind of like `FeedDescriptor` but not + */ +export type VideoFeedSourceContext = + | {type: 'feedgen'; uri: string; initialPostUri?: string} + | { + type: 'author' + did: string + filter: AuthorFilter + initialPostUri?: string + } From f20eb313375251487189169f30b2c2ec0590c274 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 16 Jan 2025 19:03:16 -0600 Subject: [PATCH 35/59] Replace types --- src/components/VideoPostCard.tsx | 16 ++-------------- src/components/feeds/PostFeedVideoGridRow.tsx | 4 ++-- src/lib/routes/types.ts | 2 +- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 97633dc13e..cc4d3124cd 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -6,9 +6,9 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeHandle} from '#/lib/strings/handles' -import {AuthorFilter} from '#/state/queries/post-feed' import {formatCount} from '#/view/com/util/numeric/format' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {atoms as a, useTheme} from '#/alf' import {BLUE_HUE} from '#/alf/util/colorGeneration' import {select} from '#/alf/util/themeSelector' @@ -19,24 +19,12 @@ import {Link} from '#/components/Link' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' -/** - * Kind of like `FeedDescriptor` but not - */ -export type SourceContext = - | {type: 'feedgen'; uri: string; initialPostUri?: string} - | { - type: 'author' - did: string - filter: AuthorFilter - initialPostUri?: string - } - export function VideoPostCard({ post, sourceContext, }: { post: AppBskyFeedDefs.PostView - sourceContext: SourceContext + sourceContext: VideoFeedSourceContext }) { const t = useTheme() const {_, i18n} = useLingui() diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index d9e2833dfa..d708dc95bf 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -2,9 +2,9 @@ import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' import {FeedPostSliceItem} from '#/state/queries/post-feed' +import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {atoms as a, useGutters} from '#/alf' import { - SourceContext, VideoPostCard, VideoPostCardPlaceholder, } from '#/components/VideoPostCard' @@ -14,7 +14,7 @@ export function PostFeedVideoGridRow({ sourceContext, }: { slices: FeedPostSliceItem[] - sourceContext: SourceContext + sourceContext: VideoFeedSourceContext }) { const gutters = useGutters(['base', 'base', 0, 'base']) const posts = slices diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 954eb30bef..fbade8223d 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -1,7 +1,7 @@ import {NavigationState, PartialState} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' -import {SourceContext as VideoFeedSourceContext} from '#/components/VideoPostCard' +import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' export type {NativeStackScreenProps} from '@react-navigation/native-stack' From 369af003fb2db23709000490df98a627f85d2cb6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 01:08:41 +0000 Subject: [PATCH 36/59] Squashed commit of the following: commit 3d1be4c0f19789dd3c5a3572ec1acd744a2edb80 Author: Samuel Newman Date: Fri Jan 17 01:08:05 2025 +0000 extend animation commit c9f199413b018efcbd9d8d2a58dd05eb41e7acb7 Author: Samuel Newman Date: Fri Jan 17 01:01:24 2025 +0000 fix gap commit 22e520795f50efda176f21a5e967cb27d0cdd907 Author: Samuel Newman Date: Fri Jan 17 00:50:16 2025 +0000 thinner bar, format time commit c32427f21405294ed3567545629a2964c4af59fe Author: Samuel Newman Date: Fri Jan 17 00:47:57 2025 +0000 fix 2 in 3 screens commit cbf84c08d64ca0a08ba9070ef5db918f89aa4296 Author: Samuel Newman Date: Fri Jan 17 00:45:46 2025 +0000 rm unneeded var commit 7e0e100177bb1cd0e64c0841bb7685c7f1eb857f Author: Samuel Newman Date: Fri Jan 17 00:41:18 2025 +0000 scrubberrrrr --- src/screens/Feeds/VibeScreen.tsx | 317 +++++++++++++++++++++++++------ 1 file changed, 263 insertions(+), 54 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 333d60596d..120f781b3c 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -8,9 +8,17 @@ import { ViewToken, } from 'react-native' import {Gesture, GestureDetector} from 'react-native-gesture-handler' -import {runOnJS} from 'react-native-reanimated' +import Animated, { + runOnJS, + runOnUI, + SharedValue, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' -import {useEvent} from 'expo' +import {useEvent, useEventListener} from 'expo' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' @@ -45,9 +53,10 @@ import {useSetMinimalShellMode} from '#/state/shell' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' +import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {Header} from '#/screens/VideoFeed/Header' -import {atoms as a, ThemeProvider, useTheme} from '#/alf' +import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' @@ -60,10 +69,13 @@ function createThreeVideoPlayers( ): [VideoPlayer, VideoPlayer, VideoPlayer] { const p1 = createVideoPlayer(sources?.[0] ?? '') p1.loop = true + p1.timeUpdateEventInterval = 0.1 const p2 = createVideoPlayer(sources?.[1] ?? '') p2.loop = true + p2.timeUpdateEventInterval = 0.1 const p3 = createVideoPlayer(sources?.[2] ?? '') p3.loop = true + p3.timeUpdateEventInterval = 0.1 return [p1, p2, p3] } @@ -369,7 +381,9 @@ function VideoItem({ ) )} {postShadow !== POST_TOMBSTONE ? ( - player && + player && ( + + ) ) : ( + active: boolean }) { const insets = useSafeAreaInsets() const t = useTheme() @@ -461,6 +477,7 @@ function Overlay({ const {status} = useEvent(player, 'statusChange', {status: player.status}) const doubleTapRef = useRef | null>(null) const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') + const seekingAnimationSV = useSharedValue(0) const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) @@ -508,59 +525,86 @@ function Overlay({ facets: record?.facets, }) + const animatedStyle = useAnimatedStyle(() => ({ + opacity: 1 - seekingAnimationSV.get(), + })) + return ( <> - - - - - - - - - {sanitizeDisplayName( - post.author.displayName || post.author.handle, - )} - - - {sanitizeHandle(post.author.handle, '@')} - + + + + + + + + + + {sanitizeDisplayName( + post.author.displayName || post.author.handle, + )} + + + {sanitizeHandle(post.author.handle, '@')} + + - - {record?.text?.trim() && ( - - )} - {record && ( - - navigation.navigate('PostThread', { - name: post.author.did, - rkey, - }) - } - big - /> - )} - - - + {record?.text?.trim() && ( + + )} + {record && ( + + navigation.navigate('PostThread', { + name: post.author.did, + rkey, + }) + } + big + /> + )} + + + + {player && active ? ( + + ) : ( + + )} + {isAndroid && status === 'loading' && ( ) } + +function Scrubber({ + player, + seekingAnimationSV, +}: { + player: VideoPlayer + seekingAnimationSV: SharedValue +}) { + const {width: screenWidth} = useWindowDimensions() + const t = useTheme() + const insets = useSafeAreaInsets() + const currentTimeSV = useSharedValue(0) + const durationSV = useSharedValue(0) + const [currentSeekTime, setCurrentSeekTime] = useState(0) + const [duration, setDuration] = useState(0) + + const updateTime = (currentTime: number, duration: number) => { + 'worklet' + currentTimeSV.set(currentTime) + durationSV.set(duration) + } + + useEventListener(player, 'timeUpdate', evt => { + runOnUI(updateTime)(evt.currentTime, player.duration) + }) + + const isSeekingSV = useSharedValue(false) + const seekProgressSV = useSharedValue(0) + + useAnimatedReaction( + () => Math.round(seekProgressSV.get()), + (progress, prevProgress) => { + if (progress !== prevProgress) { + runOnJS(setCurrentSeekTime)(progress) + } + }, + ) + + useAnimatedReaction( + () => Math.round(durationSV.get()), + (duration, prevDuration) => { + if (duration !== prevDuration) { + runOnJS(setDuration)(duration) + } + }, + ) + + const seekBy = useCallback( + (time: number) => { + player.seekBy(time) + }, + [player], + ) + + const gesture = useMemo(() => { + return Gesture.Pan() + .failOffsetY([-10, 10]) + .onStart(() => { + 'worklet' + seekProgressSV.set(currentTimeSV.get()) + isSeekingSV.set(true) + seekingAnimationSV.set(withTiming(1, {duration: 500})) + }) + .onUpdate(evt => { + 'worklet' + const progress = evt.x / screenWidth + seekProgressSV.set( + clamp(progress * durationSV.get(), 0, durationSV.get()), + ) + }) + .onEnd(evt => { + 'worklet' + isSeekingSV.get() + + const progress = evt.x / screenWidth + const newTime = clamp(progress * durationSV.get(), 0, durationSV.get()) + + // it's seek by, so offset by the current time + runOnJS(seekBy)(newTime - currentTimeSV.get()) + + isSeekingSV.set(false) + seekingAnimationSV.set(withTiming(0, {duration: 500})) + }) + }, [ + seekingAnimationSV, + seekBy, + screenWidth, + currentTimeSV, + durationSV, + isSeekingSV, + seekProgressSV, + ]) + + const timeStyle = useAnimatedStyle(() => { + return { + display: seekingAnimationSV.get() === 0 ? 'none' : 'flex', + opacity: seekingAnimationSV.get(), + } + }) + + const barStyle = useAnimatedStyle(() => { + const currentTime = isSeekingSV.get() + ? seekProgressSV.get() + : currentTimeSV.get() + const progress = currentTime === 0 ? 0 : currentTime / durationSV.get() + return { + height: seekingAnimationSV.get() * 3 + 1, + width: `${progress * 100}%`, + } + }) + + return ( + <> + + + + {formatTime(currentSeekTime)} + + + {' / '} + + + {formatTime(duration)} + + + + + + + + + + ) +} + +function clamp(num: number, min: number, max: number) { + 'worklet' + return Math.min(Math.max(num, min), max) +} From 706a95491d0d606f8f7795541df91515cfbf72e2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 01:45:45 +0000 Subject: [PATCH 37/59] use white with opacity rather than gray --- src/screens/Feeds/VibeScreen.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 120f781b3c..ca0db1a07e 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -686,7 +686,6 @@ function Scrubber({ seekingAnimationSV: SharedValue }) { const {width: screenWidth} = useWindowDimensions() - const t = useTheme() const insets = useSafeAreaInsets() const currentTimeSV = useSharedValue(0) const durationSV = useSharedValue(0) @@ -805,13 +804,11 @@ function Scrubber({ {formatTime(currentSeekTime)} - - {' / '} - + {' / '} {formatTime(duration)} From 1fcc71603558386916585f02a746b4d0c5e3be83 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 17 Jan 2025 03:23:34 +0000 Subject: [PATCH 38/59] Simultaneous gesture --- src/screens/Feeds/VibeScreen.tsx | 73 ++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index ca0db1a07e..b80a43e0ca 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -7,7 +7,11 @@ import { View, ViewToken, } from 'react-native' -import {Gesture, GestureDetector} from 'react-native-gesture-handler' +import { + Gesture, + GestureDetector, + NativeGesture, +} from 'react-native-gesture-handler' import Animated, { runOnJS, runOnUI, @@ -157,6 +161,8 @@ function Inner() { const [currentIndex, setCurrentIndex] = useState(0) + const scrollGesture = useMemo(() => Gesture.Native(), []) + const renderItem: ListRenderItem = useCallback( ({item, index}) => { const {post} = item @@ -177,10 +183,11 @@ function Inner() { index === currentIndex && currentSource === post.embed.playlist } + scrollGesture={scrollGesture} /> ) }, - [players, currentIndex, isFocused, currentSources], + [players, currentIndex, isFocused, currentSources, scrollGesture], ) const updateVideoState = useNonReactiveCallback((index?: number) => { @@ -307,29 +314,31 @@ function Inner() { ) return ( - - } - onEndReached={() => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage() + + } - }} - showsVerticalScrollIndicator={false} - onViewableItemsChanged={onViewableItemsChanged} - viewabilityConfig={{itemVisiblePercentThreshold: 95}} - /> + onEndReached={() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }} + showsVerticalScrollIndicator={false} + onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={{itemVisiblePercentThreshold: 95}} + /> + ) } @@ -342,11 +351,13 @@ function VideoItem({ post, embed, active, + scrollGesture, }: { player?: VideoPlayer post: AppBskyFeedDefs.PostView embed: AppBskyEmbedVideo.View active: boolean + scrollGesture: NativeGesture }) { const postShadow = usePostShadow(post) const {height, width} = useWindowDimensions() @@ -382,7 +393,12 @@ function VideoItem({ )} {postShadow !== POST_TOMBSTONE ? ( player && ( - + ) ) : ( active: boolean + scrollGesture: NativeGesture }) { const insets = useSafeAreaInsets() const t = useTheme() @@ -504,19 +522,20 @@ function Overlay({ const gesture = useMemo(() => { const dragLeftGesture = Gesture.Pan() + .simultaneousWithExternalGesture(scrollGesture) .activeOffsetX([0, 10]) .failOffsetX([-10, 0]) .failOffsetY([-5, 5]) .maxPointers(1) .onEnd(evt => { 'worklet' - if (evt.translationX < -50) { + if (evt.translationX < -50 && evt.velocityX < -300) { runOnJS(pushToProfile)() } }) return dragLeftGesture - }, [pushToProfile]) + }, [pushToProfile, scrollGesture]) const rkey = new AtUri(post.uri).rkey const record = AppBskyFeedPost.isRecord(post.record) ? post.record : undefined From e466bf6b5cbe77ca054433a9a025968d8c5bcb5e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 16:47:46 +0000 Subject: [PATCH 39/59] cleanup attempt --- src/screens/Feeds/VibeScreen.tsx | 416 ++++++++++++++++--------------- 1 file changed, 210 insertions(+), 206 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index b80a43e0ca..a6cc0910cb 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -22,8 +22,8 @@ import Animated, { withTiming, } from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' -import {useEvent, useEventListener} from 'expo' -import {Image} from 'expo-image' +import {useEventListener} from 'expo' +import {Image, ImageStyle} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import { @@ -49,7 +49,6 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' -import {isAndroid} from '#/platform/detection' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {usePostLikeMutationQueue} from '#/state/queries/post' import {FeedPostSliceItem, usePostFeedQuery} from '#/state/queries/post-feed' @@ -58,13 +57,12 @@ import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' -import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {Header} from '#/screens/VideoFeed/Header' import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' -import {Loader} from '#/components/Loader' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' @@ -361,46 +359,11 @@ function VideoItem({ }) { const postShadow = usePostShadow(post) const {height, width} = useWindowDimensions() - const insets = useSafeAreaInsets() return ( - + - {player ? ( - - ) : ( - embed.thumbnail && ( - - ) - )} - {postShadow !== POST_TOMBSTONE ? ( - player && ( - - ) - ) : ( + {postShadow === POST_TOMBSTONE ? ( Post has been deleted + ) : ( + <> + + {player && ( + + )} + + )} ) } +// TODO maybe unused function VideoItemInner({ player, embed, @@ -436,44 +413,27 @@ function VideoItemInner({ embed: AppBskyEmbedVideo.View active: boolean }) { - const insets = useSafeAreaInsets() - const {status} = useEvent(player, 'statusChange', {status: player.status}) - - const videoAspectRatio = - (embed.aspectRatio?.width ?? 1) / (embed.aspectRatio?.height ?? 1) - - // if the video tall enough (tiktok/reels are 9:16) go cover mode - const isCloseEnough = videoAspectRatio <= 9 / 16 + // const {status} = useEvent(player, 'statusChange', {status: player.status}) return ( <> + {/* + + */} + {active && player && ( )} - {embed.thumbnail && ( - - )} ) } @@ -484,42 +444,20 @@ function Overlay({ active, scrollGesture, }: { - player: VideoPlayer + player?: VideoPlayer post: Shadow active: boolean scrollGesture: NativeGesture }) { - const insets = useSafeAreaInsets() const t = useTheme() const navigation = useNavigation() - const {status} = useEvent(player, 'statusChange', {status: player.status}) - const doubleTapRef = useRef | null>(null) - const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') + // const {status} = useEvent(player, 'statusChange', {status: player.status}) const seekingAnimationSV = useSharedValue(0) const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) }) - const togglePlayPause = () => { - doubleTapRef.current = null - if (player.playing) { - player.pause() - } else { - player.play() - } - } - - const onPress = () => { - if (doubleTapRef.current) { - clearTimeout(doubleTapRef.current) - doubleTapRef.current = null - queueLike() - } else { - doubleTapRef.current = setTimeout(togglePlayPause, 200) - } - } - const gesture = useMemo(() => { const dragLeftGesture = Gesture.Pan() .simultaneousWithExternalGesture(scrollGesture) @@ -552,49 +490,40 @@ function Overlay({ <> - - - - - - - - {sanitizeDisplayName( - post.author.displayName || post.author.handle, - )} - - - {sanitizeHandle(post.author.handle, '@')} - - + + + + + + + + + + + + {sanitizeDisplayName( + post.author.displayName || post.author.handle, + )} + + + {sanitizeHandle(post.author.handle, '@')} + - {record?.text?.trim() && ( + + {record?.text?.trim() && ( + - )} - {record && ( + + )} + {record && ( + - )} - + + )} - - {player && active ? ( - - ) : ( - - )} + + {player && active ? ( + + ) : ( + + )} + + {/* {isAndroid && status === 'loading' && ( )} + */} ) } -function ExpandableRichTextView({ - value, - authorHandle, -}: { - value: RichTextAPI - authorHandle?: string -}) { - const {height: screenHeight} = useWindowDimensions() - const [expanded, setExpanded] = useState(false) - const [constrained, setConstrained] = useState(false) - const [contentHeight, setContentHeight] = useState(0) - const {_} = useLingui() - - return ( - { - if (expanded) { - LayoutAnimation.configureNext({ - duration: 500, - update: {type: 'spring', springDamping: 0.6}, - }) - } - setContentHeight(h) - }} - style={{height: Math.min(contentHeight, screenHeight * 0.5)}} - contentContainerStyle={[ - a.gap_xs, - expanded ? [a.align_start] : a.flex_row, - ]}> - { - if (!constrained && evt.nativeEvent.lines.length > 1) { - setConstrained(true) - } - }} - /> - {constrained && ( - - )} - - ) +/** + * Magic number that matches the Scrubber height + */ +function ScrubberPlaceholder() { + return } function Scrubber({ @@ -814,7 +691,7 @@ function Scrubber({ { left: 0, right: 0, - bottom: tokens.space.xl + tokens.space._4xl + insets.bottom, + bottom: insets.bottom + 48, }, timeStyle, ]} @@ -834,15 +711,18 @@ function Scrubber({ + { + if (expanded) { + LayoutAnimation.configureNext({ + duration: 500, + update: {type: 'spring', springDamping: 0.6}, + }) + } + setContentHeight(h) + }} + style={{height: Math.min(contentHeight, screenHeight * 0.5)}} + contentContainerStyle={[ + a.gap_xs, + expanded ? [a.align_start] : a.flex_row, + ]}> + { + if (!constrained && evt.nativeEvent.lines.length > 1) { + setConstrained(true) + } + }} + /> + {constrained && ( + + )} + + ) +} + +function VideoItemPlaceholder({ + embed, + style, +}: { + embed: AppBskyEmbedVideo.View + style?: ImageStyle +}) { + const src = embed.thumbnail + return src ? ( + + ) : null +} + +function PlayPauseTapArea({ + player, + post, +}: { + player?: VideoPlayer + post: Shadow +}) { + const doubleTapRef = useRef | null>(null) + const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') + const togglePlayPause = () => { + if (!player) return + doubleTapRef.current = null + if (player.playing) { + player.pause() + } else { + player.play() + } + } + + const onPress = () => { + if (doubleTapRef.current) { + clearTimeout(doubleTapRef.current) + doubleTapRef.current = null + queueLike() + } else { + doubleTapRef.current = setTimeout(togglePlayPause, 200) + } + } + + return ( + + ) +} + function clamp(num: number, min: number, max: number) { 'worklet' return Math.min(Math.max(num, min), max) } + +/* + * If the video is taller than 9:16 + */ +function isTallAspectRatio(aspectRatio: AppBskyEmbedVideo.View['aspectRatio']) { + const videoAspectRatio = + (aspectRatio?.width ?? 1) / (aspectRatio?.height ?? 1) + return videoAspectRatio <= 9 / 16 +} From adf8697a617be248608e480d7c7edb582f21749a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 16:56:28 +0000 Subject: [PATCH 40/59] fix jank --- src/screens/Feeds/VibeScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index a6cc0910cb..7cc6ec0d46 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -334,7 +334,7 @@ function Inner() { }} showsVerticalScrollIndicator={false} onViewableItemsChanged={onViewableItemsChanged} - viewabilityConfig={{itemVisiblePercentThreshold: 95}} + viewabilityConfig={{itemVisiblePercentThreshold: 100}} /> ) From a0215885ed65282702978a3466045469cf73ed44 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 16:58:48 +0000 Subject: [PATCH 41/59] link to profile on press --- src/screens/Feeds/VibeScreen.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 7cc6ec0d46..39d6605784 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -62,6 +62,7 @@ import {Header} from '#/screens/VideoFeed/Header' import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' +import {Link} from '#/components/Link' import {ListFooter} from '#/components/Lists' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' @@ -449,6 +450,7 @@ function Overlay({ active: boolean scrollGesture: NativeGesture }) { + const {_} = useLingui() const t = useTheme() const navigation = useNavigation() // const {status} = useEvent(player, 'statusChange', {status: player.status}) @@ -499,7 +501,17 @@ function Overlay({ colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.8)', 'rgba(0,0,0,0.95)']} style={[a.w_full, a.pt_md]}> - + @@ -513,7 +525,7 @@ function Overlay({ {sanitizeHandle(post.author.handle, '@')} - + {record?.text?.trim() && ( Date: Fri, 17 Jan 2025 17:39:09 +0000 Subject: [PATCH 42/59] fix jitter fr this time --- src/screens/Feeds/VibeScreen.tsx | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 39d6605784..737d374cd6 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -453,7 +453,6 @@ function Overlay({ const {_} = useLingui() const t = useTheme() const navigation = useNavigation() - // const {status} = useEvent(player, 'statusChange', {status: player.status}) const seekingAnimationSV = useSharedValue(0) const pushToProfile = useNonReactiveCallback(() => { @@ -583,7 +582,18 @@ function Overlay({ * Magic number that matches the Scrubber height */ function ScrubberPlaceholder() { - return + const {bottom} = useSafeAreaInsets() + return ( + + ) } function Scrubber({ @@ -732,14 +742,19 @@ function Scrubber({ a.pt_lg, a.justify_end, { - height: tokens.space.md, - paddingBottom: insets.bottom + 12, + paddingBottom: insets.bottom + tokens.space.md, + height: + // bottom padding + insets.bottom + + tokens.space.md + + // actual height + tokens.space.md + + // top padding + tokens.space.lg, }, a.z_10, ]}> - + From be48da87cb34b7a3c431b1bb0894481a31532dca Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 17:59:51 +0000 Subject: [PATCH 43/59] mostly fix android flicker --- src/screens/Feeds/VibeScreen.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 737d374cd6..2852f94cf6 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -22,7 +22,7 @@ import Animated, { withTiming, } from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' -import {useEventListener} from 'expo' +import {useEvent, useEventListener} from 'expo' import {Image, ImageStyle} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' @@ -49,6 +49,7 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {isAndroid} from '#/platform/detection' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {usePostLikeMutationQueue} from '#/state/queries/post' import {FeedPostSliceItem, usePostFeedQuery} from '#/state/queries/post-feed' @@ -404,7 +405,6 @@ function VideoItem({ ) } -// TODO maybe unused function VideoItemInner({ player, embed, @@ -414,19 +414,17 @@ function VideoItemInner({ embed: AppBskyEmbedVideo.View active: boolean }) { - // const {status} = useEvent(player, 'statusChange', {status: player.status}) + const {status} = useEvent(player, 'statusChange', {status: player.status}) return ( <> - {/* - - */} - {active && player && ( Date: Fri, 17 Jan 2025 11:58:03 -0600 Subject: [PATCH 44/59] Maybe fix row generation --- src/view/com/posts/PostFeed.tsx | 65 ++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 8bb9d7162c..84edfcf630 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -323,37 +323,44 @@ let PostFeed = ({ }) } else if (data) { let sliceIndex = -1 - for (const page of data?.pages) { - if (isVideoFeed && isNative) { - const rows: FeedPostSliceItem[][] = [] - for (let i = 0; i < page.slices.length; i++) { - const slice = page.slices[i] - const root = slice.items.at(0) - if (!root) continue - // TODO test this - if (!AppBskyEmbedVideo.isView(root.post.embed)) { - i-- - continue - } - const cols = gtMobile ? 3 : 2 - if (i % cols === 0) { - rows.push([root]) - } else { - rows[rows.length - 1].push(root) - } - } - for (const row of rows) { - sliceIndex++ - - arr.push({ - type: 'videoGridRow', - key: row.map(r => r._reactKey).join('-'), - slices: row, - sourceFeedUri: feedUri, - }) + if (isVideoFeed && isNative) { + const rows: FeedPostSliceItem[][] = [] + let slices: {slice: FeedPostSlice; index: number}[] = [] + for (const page of data.pages) { + for (const slice of page.slices) { + slices.push({slice, index: sliceIndex++}) } - } else { + } + + for (let i = 0; i < slices.length; i++) { + const slice = slices[i] + const root = slice.slice.items.at(0) + if (!root) continue + // TODO test this + if (!AppBskyEmbedVideo.isView(root.post.embed)) { + i-- + continue + } + const cols = gtMobile ? 3 : 2 + if (i % cols === 0) { + rows.push([root]) + } else { + rows[rows.length - 1].push(root) + } + } + + for (const row of rows) { + sliceIndex++ + arr.push({ + type: 'videoGridRow', + key: row.map(r => r._reactKey).join('-'), + slices: row, + sourceFeedUri: feedUri, + }) + } + } else { + for (const page of data?.pages) { for (const slice of page.slices) { sliceIndex++ From 29c7c7973a142456a1e1f7080a310f1c04f09c1c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 11:58:22 -0600 Subject: [PATCH 45/59] Add content hider to video card --- src/components/VideoPostCard.tsx | 269 ++++++++++++------ src/components/feeds/PostFeedVideoGridRow.tsx | 13 +- 2 files changed, 188 insertions(+), 94 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index cc4d3124cd..e5137d84fe 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -1,30 +1,40 @@ import {View} from 'react-native' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' -import {AppBskyEmbedVideo, AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' +import { + AppBskyActorDefs, + AppBskyEmbedVideo, + AppBskyFeedDefs, + AppBskyFeedPost, + ModerationDecision, +} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeHandle} from '#/lib/strings/handles' import {formatCount} from '#/view/com/util/numeric/format' -import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {atoms as a, useTheme} from '#/alf' import {BLUE_HUE} from '#/alf/util/colorGeneration' import {select} from '#/alf/util/themeSelector' import {useInteractionState} from '#/components/hooks/useInteractionState' +import {EyeSlash_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/EyeSlash' import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2' import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' import {Link} from '#/components/Link' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' export function VideoPostCard({ post, sourceContext, + moderation, }: { post: AppBskyFeedDefs.PostView sourceContext: VideoFeedSourceContext + moderation: ModerationDecision }) { const t = useTheme() const {_, i18n} = useLingui() @@ -70,82 +80,129 @@ export function VideoPostCard({ justifyContent: undefined, }, ]}> - - - - - + + - - - - {likeCount > 0 && ( - - - - {formatCount(i18n, likeCount)} - - - )} - {repostCount > 0 && ( - - - - {formatCount(i18n, repostCount)} - - - )} + + + + + + + {_(msg`Hidden`)} + + - - - - {text && ( - - {text} - - )} - - - + + + - {sanitizeHandle(post.author.handle, '@')} - - - + a.justify_center, + a.rounded_md, + a.overflow_hidden, + { + backgroundColor: black, + aspectRatio: 9 / 16, + }, + ]}> + + + + + + + + + {likeCount > 0 && ( + + + + {formatCount(i18n, likeCount)} + + + )} + {repostCount > 0 && ( + + + + {formatCount(i18n, repostCount)} + + + )} + + + + + + {text && ( + + {text} + + )} + + + + {sanitizeHandle(post.author.handle, '@')} + + + + + ) } @@ -171,6 +228,20 @@ export function VideoPostCardPlaceholder() { ]}> + + + ) +} + +export function VideoPostCardTextPlaceholder({ + author, +}: { + author?: AppBskyActorDefs.ProfileViewBasic +}) { + const t = useTheme() + + return ( + - - - - + {author ? ( + + + + {sanitizeHandle(author.handle, '@')} + + + ) : ( + + + + + )} ) diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index d708dc95bf..88a03ddaa3 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -19,7 +19,10 @@ export function PostFeedVideoGridRow({ const gutters = useGutters(['base', 'base', 0, 'base']) const posts = slices .filter(slice => AppBskyEmbedVideo.isView(slice.post.embed)) - .map(slice => slice.post) + .map(slice => ({ + post: slice.post, + moderation: slice.moderation, + })) /** * This should not happen because we should be filtering out posts without @@ -31,8 +34,12 @@ export function PostFeedVideoGridRow({ {posts.map(post => ( - - + + ))} From 32f6feb0cea51dcdd5cbad6e16fe11ac1b8815f0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 18:28:44 +0000 Subject: [PATCH 46/59] emoji in post text --- src/components/VideoPostCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index e5137d84fe..5b27887b73 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -183,7 +183,7 @@ export function VideoPostCard({ {text && ( - + {text} )} From 593c56f3a3270041ea168b1975094d9b8e43aac2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 19:04:48 +0000 Subject: [PATCH 47/59] reduce update rate --- src/screens/Feeds/VibeScreen.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 2852f94cf6..529fbe5df5 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -73,13 +73,13 @@ function createThreeVideoPlayers( ): [VideoPlayer, VideoPlayer, VideoPlayer] { const p1 = createVideoPlayer(sources?.[0] ?? '') p1.loop = true - p1.timeUpdateEventInterval = 0.1 + p1.timeUpdateEventInterval = 0.25 const p2 = createVideoPlayer(sources?.[1] ?? '') p2.loop = true - p2.timeUpdateEventInterval = 0.1 + p2.timeUpdateEventInterval = 0.25 const p3 = createVideoPlayer(sources?.[2] ?? '') p3.loop = true - p3.timeUpdateEventInterval = 0.1 + p3.timeUpdateEventInterval = 0.25 return [p1, p2, p3] } From e5c576ac84cb4357c86dd8c7a5bab8d4589b309a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 19:18:09 +0000 Subject: [PATCH 48/59] fix type error --- src/screens/Feeds/VibeScreen.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 529fbe5df5..ee587e4396 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -52,7 +52,11 @@ import {sanitizeHandle} from '#/lib/strings/handles' import {isAndroid} from '#/platform/detection' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {usePostLikeMutationQueue} from '#/state/queries/post' -import {FeedPostSliceItem, usePostFeedQuery} from '#/state/queries/post-feed' +import { + AuthorFilter, + FeedPostSliceItem, + usePostFeedQuery, +} from '#/state/queries/post-feed' import {useSetMinimalShellMode} from '#/state/shell' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' @@ -125,9 +129,11 @@ function Inner() { const feedDesc = useMemo(() => { switch (params.type) { case 'feedgen': - return `feedgen|${params.uri}` as const + return `feedgen|${params.uri as string}` as const case 'author': - return `author|${params.did}|${params.filter}` as const + return `author|${params.did as string}|${ + params.filter as AuthorFilter + }` as const default: throw new Error(`Invalid video feed params ${JSON.stringify(params)}`) } From a6a81bf0a8385c42ef966731e950baed23d64150 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 12:46:08 -0600 Subject: [PATCH 49/59] Fix grid layout trailing single item --- src/components/Grid.tsx | 59 +++++++++++++++++++ src/components/feeds/PostFeedVideoGridRow.tsx | 21 ++++--- 2 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 src/components/Grid.tsx diff --git a/src/components/Grid.tsx b/src/components/Grid.tsx new file mode 100644 index 0000000000..d71a46c0b7 --- /dev/null +++ b/src/components/Grid.tsx @@ -0,0 +1,59 @@ +import {createContext, useContext,useMemo} from 'react' +import {View} from 'react-native' + +import {atoms as a, ViewStyleProp} from '#/alf' + +const Context = createContext({ + gap: 0, +}) + +export function Row({ + children, + gap = 0, + style, +}: ViewStyleProp & { + children: React.ReactNode + gap?: number +}) { + return ( + ({gap}), [gap])}> + + {children} + + + ) +} + +export function Col({ + children, + width = 1, + style, +}: ViewStyleProp & { + children: React.ReactNode + width?: number +}) { + const {gap} = useContext(Context) + return ( + + {children} + + ) +} diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index 88a03ddaa3..688fb961a1 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -4,6 +4,7 @@ import {AppBskyEmbedVideo} from '@atproto/api' import {FeedPostSliceItem} from '#/state/queries/post-feed' import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {atoms as a, useGutters} from '#/alf' +import * as Grid from '#/components/Grid' import { VideoPostCard, VideoPostCardPlaceholder, @@ -33,15 +34,17 @@ export function PostFeedVideoGridRow({ return ( - {posts.map(post => ( - - - - ))} + + {posts.map(post => ( + + + + ))} + ) From e83f962b82183764e7b5c8bf12cfe9fa97d0e6e4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 15:27:52 -0600 Subject: [PATCH 50/59] Add Discover interstitial, settings, includes pin for now --- src/components/VideoPostCard.tsx | 79 +++++--- .../interstitials/TrendingVideos.tsx | 184 ++++++++++++++++++ src/lib/statsig/events.ts | 9 + .../Settings/ContentAndMediaSettings.tsx | 26 ++- src/state/persisted/schema.ts | 2 + src/state/preferences/trending.tsx | 20 +- src/view/com/posts/PostFeed.tsx | 17 +- 7 files changed, 304 insertions(+), 33 deletions(-) create mode 100644 src/components/interstitials/TrendingVideos.tsx diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 5b27887b73..7c18cb72b3 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -27,14 +27,23 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' +type DisplayVariant = 'default' | 'compact' + export function VideoPostCard({ post, sourceContext, moderation, + variant = 'default', + onInteract, }: { post: AppBskyFeedDefs.PostView sourceContext: VideoFeedSourceContext moderation: ModerationDecision + variant?: DisplayVariant + /** + * Callback for metrics and stuff + */ + onInteract: () => void }) { const t = useTheme() const {_, i18n} = useLingui() @@ -51,6 +60,7 @@ export function VideoPostCard({ */ if (!AppBskyEmbedVideo.isView(embed)) return null + const isCompact = variant === 'compact' const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : '' const likeCount = post?.likeCount ?? 0 const repostCount = post?.repostCount ?? 0 @@ -71,6 +81,9 @@ export function VideoPostCard({ initialPostUri: post.uri, }, }} + onPress={() => { + onInteract() + }} onPressIn={onPressIn} onPressOut={onPressOut} style={[ @@ -169,7 +182,7 @@ export function VideoPostCard({ )} - {repostCount > 0 && ( + {!isCompact && repostCount > 0 && ( @@ -182,7 +195,7 @@ export function VideoPostCard({ - {text && ( + {!isCompact && text && ( {text} @@ -207,7 +220,11 @@ export function VideoPostCard({ ) } -export function VideoPostCardPlaceholder() { +export function VideoPostCardPlaceholder({ + variant = 'default', +}: { + variant?: DisplayVariant +}) { const t = useTheme() const black = select(t.name, { light: t.palette.black, @@ -228,42 +245,49 @@ export function VideoPostCardPlaceholder() { ]}> - + ) } export function VideoPostCardTextPlaceholder({ author, + variant, }: { author?: AppBskyActorDefs.ProfileViewBasic + variant?: DisplayVariant }) { const t = useTheme() + const isCompact = variant === 'compact' return ( - - + {!isCompact && ( + <> + + + + )} {author ? ( @@ -298,8 +322,9 @@ export function VideoPostCardTextPlaceholder({ height: 12, width: '75%', }, - ]} - /> + ]}> + + )} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx new file mode 100644 index 0000000000..ee62da41c0 --- /dev/null +++ b/src/components/interstitials/TrendingVideos.tsx @@ -0,0 +1,184 @@ +import React from 'react' +import {ScrollView,View} from 'react-native' +import {AppBskyEmbedVideo} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {VIDEO_FEED_URI} from '#/lib/constants' +import {logEvent} from '#/lib/statsig/statsig' +import {useTrendingSettingsApi} from '#/state/preferences/trending' +import {useSavedFeeds} from '#/state/queries/feed' +import {usePostFeedQuery} from '#/state/queries/post-feed' +import {useAddSavedFeedsMutation} from '#/state/queries/preferences' +import {atoms as a, useGutters,useTheme} from '#/alf' +import {Button, ButtonIcon,ButtonText} from '#/components/Button' +import {Divider} from '#/components/Divider' +import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import {Text} from '#/components/Typography' +import { + VideoPostCard, + VideoPostCardPlaceholder, +} from '#/components/VideoPostCard' + +const CARD_WIDTH = 100 + +export function TrendingVideos() { + const t = useTheme() + const {_} = useLingui() + const gutters = useGutters([0, 'base']) + const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`) + const {setTrendingVideoDisabled} = useTrendingSettingsApi() + + const {data: saved} = useSavedFeeds() + const isSavedAlready = React.useMemo(() => { + return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI) + }, [saved]) + + const {mutateAsync: addSavedFeeds, isPending: isPinPending} = + useAddSavedFeedsMutation() + const pinFeed = React.useCallback( + (e: any) => { + e.preventDefault() + + addSavedFeeds([ + { + type: 'feed', + value: VIDEO_FEED_URI, + pinned: true, + }, + ]) + + // prevent navigation + return false + }, + [addSavedFeeds], + ) + + const hide = React.useCallback(() => { + setTrendingVideoDisabled(true) + logEvent('trendingVideos:hide', {context: 'interstitial'}) + }, [setTrendingVideoDisabled]) + + if (error) { + return null + } + + return ( + + + + + + Trending Videos + + + + + + + + {isLoading ? ( + Array(10) + .fill(0) + .map((_, i) => ( + + + + )) + ) : error || !data ? ( + + Whoops! Trending videos failed to load. + + ) : ( + data.pages + .flatMap(page => page.slices) + .map(slice => slice.items[0]) + .filter(Boolean) + .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) + .map(item => ( + + { + logEvent('trendingVideo:click', {context: 'interstitial'}) + }} + /> + + )) + )} + + + + {!isSavedAlready && ( + + + + + + Pin to your home screen for easy access + + + + + )} + + ) +} diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index f914ef01f9..ca1f683e98 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -249,6 +249,15 @@ export type LogEvents = { 'recommendedTopic:click': { context: 'explore' } + 'trendingVideos:show': { + context: 'settings' + } + 'trendingVideos:hide': { + context: 'settings' | 'interstitial' | 'explore' + } + 'trendingVideo:click': { + context: 'interstitial' | 'explore' + } 'progressGuide:hide': {} 'progressGuide:followDialog:open': {} diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index 4a9354bb84..415801370d 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -37,8 +37,9 @@ export function ContentAndMediaSettingsScreen({}: Props) { const inAppBrowserPref = useInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser() const {enabled: trendingEnabled} = useTrendingConfig() - const {trendingDisabled} = useTrendingSettings() - const {setTrendingDisabled} = useTrendingSettingsApi() + const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() + const {setTrendingDisabled, setTrendingVideoDisabled} = + useTrendingSettingsApi() return ( @@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) { + { + const hide = Boolean(!value) + if (hide) { + logEvent('trendingVideos:hide', {context: 'settings'}) + } else { + logEvent('trendingVideos:show', {context: 'settings'}) + } + setTrendingVideoDisabled(hide) + }}> + + + + Enable trending videos + + + + )} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 0a9e5b2c07..f840081f3d 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -126,6 +126,7 @@ const schema = z.object({ /** @deprecated */ mutedThreads: z.array(z.string()), trendingDisabled: z.boolean().optional(), + trendingVideoDisabled: z.boolean().optional(), }) export type Schema = z.infer @@ -172,6 +173,7 @@ export const defaults: Schema = { hasCheckedForStarterPack: false, subtitlesEnabled: true, trendingDisabled: false, + trendingVideoDisabled: false, } export function tryParse(rawData: string): Schema | undefined { diff --git a/src/state/preferences/trending.tsx b/src/state/preferences/trending.tsx index bf5d8f13cc..87ec687712 100644 --- a/src/state/preferences/trending.tsx +++ b/src/state/preferences/trending.tsx @@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted' type StateContext = { trendingDisabled: Exclude + trendingVideoDisabled: Exclude< + persisted.Schema['trendingVideoDisabled'], + undefined + > } type ApiContext = { setTrendingDisabled( hidden: Exclude, ): void + setTrendingVideoDisabled( + hidden: Exclude, + ): void } const StateContext = React.createContext({ trendingDisabled: Boolean(persisted.defaults.trendingDisabled), + trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled), }) const ApiContext = React.createContext({ setTrendingDisabled() {}, + setTrendingVideoDisabled() {}, }) function usePersistedBooleanValue(key: T) { @@ -43,14 +52,19 @@ function usePersistedBooleanValue(key: T) { export function Provider({children}: React.PropsWithChildren<{}>) { const [trendingDisabled, setTrendingDisabled] = usePersistedBooleanValue('trendingDisabled') + const [trendingVideoDisabled, setTrendingVideoDisabled] = + usePersistedBooleanValue('trendingVideoDisabled') /* * Context */ - const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled]) + const state = React.useMemo( + () => ({trendingDisabled, trendingVideoDisabled}), + [trendingDisabled, trendingVideoDisabled], + ) const api = React.useMemo( - () => ({setTrendingDisabled}), - [setTrendingDisabled], + () => ({setTrendingDisabled, setTrendingVideoDisabled}), + [setTrendingDisabled, setTrendingVideoDisabled], ) return ( diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 84edfcf630..c9ed8a08da 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -50,6 +50,7 @@ import { PostFeedVideoGridRowPlaceholder, } from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' +import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' import {PostFeedErrorMessage} from './PostFeedErrorMessage' @@ -116,6 +117,10 @@ type FeedRow = type: 'interstitialTrending' key: string } + | { + type: 'interstitialTrendingVideos' + key: string + } export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null { if (feedRow.type === 'sliceItem') { @@ -287,7 +292,7 @@ let PostFeed = ({ const showProgressIntersitial = (followProgressGuide || followAndLikeProgressGuide) && !isDesktop - const {trendingDisabled} = useTrendingSettings() + const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() const feedItems: FeedRow[] = React.useMemo(() => { let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined @@ -380,6 +385,13 @@ let PostFeed = ({ 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, }) } + } else if (sliceIndex === 15) { + if (isNative && !trendingVideoDisabled) { + arr.push({ + type: 'interstitialTrendingVideos', + key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, + }) + } } else if (sliceIndex === 30) { arr.push({ type: 'interstitialFollows', @@ -475,6 +487,7 @@ let PostFeed = ({ hasSession, showProgressIntersitial, trendingDisabled, + trendingVideoDisabled, gtTablet, gtMobile, isVideoFeed, @@ -566,6 +579,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'interstitialTrendingVideos') { + return } else if (row.type === 'sliceItem') { const slice = row.slice if (slice.isFallbackMarker) { From dffee47703cc06c9c4cebe20db4c2ffa315df053 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 16:17:58 -0600 Subject: [PATCH 51/59] Explore interstitial, handle dimissal, pinning, compact card --- src/components/VideoPostCard.tsx | 290 ++++++++++++++---- .../interstitials/TrendingVideos.tsx | 88 ++---- .../components/ExploreTrendingVideos.tsx | 175 +++++++++++ src/view/screens/Search/Explore.tsx | 17 +- 4 files changed, 443 insertions(+), 127 deletions(-) create mode 100644 src/screens/Search/components/ExploreTrendingVideos.tsx diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 7c18cb72b3..4b75354e17 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -27,21 +27,25 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' -type DisplayVariant = 'default' | 'compact' +function getBlackColor(t: ReturnType) { + return select(t.name, { + light: t.palette.black, + dark: t.atoms.bg_contrast_25.backgroundColor, + dim: `hsl(${BLUE_HUE}, 28%, 6%)`, + }) +} export function VideoPostCard({ post, sourceContext, moderation, - variant = 'default', onInteract, }: { post: AppBskyFeedDefs.PostView sourceContext: VideoFeedSourceContext moderation: ModerationDecision - variant?: DisplayVariant /** - * Callback for metrics and stuff + * Callback for metrics etc */ onInteract: () => void }) { @@ -60,16 +64,11 @@ export function VideoPostCard({ */ if (!AppBskyEmbedVideo.isView(embed)) return null - const isCompact = variant === 'compact' const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : '' const likeCount = post?.likeCount ?? 0 const repostCount = post?.repostCount ?? 0 const {thumbnail} = embed - const black = select(t.name, { - light: t.palette.black, - dark: t.atoms.bg_contrast_25.backgroundColor, - dim: `hsl(${BLUE_HUE}, 28%, 6%)`, - }) + const black = getBlackColor(t) return ( )} - {!isCompact && repostCount > 0 && ( + {repostCount > 0 && ( @@ -195,13 +194,17 @@ export function VideoPostCard({ - {!isCompact && text && ( + {text && ( {text} )} - + + + + @@ -245,52 +240,48 @@ export function VideoPostCardPlaceholder({ ]}> - + ) } export function VideoPostCardTextPlaceholder({ author, - variant, }: { author?: AppBskyActorDefs.ProfileViewBasic - variant?: DisplayVariant }) { const t = useTheme() - const isCompact = variant === 'compact' return ( - {!isCompact && ( - <> - - - - )} + + {author ? ( - + + + + - - + ]} + /> )} ) } + +export function CompactVideoPostCard({ + post, + sourceContext, + moderation, + onInteract, +}: { + post: AppBskyFeedDefs.PostView + sourceContext: VideoFeedSourceContext + moderation: ModerationDecision + /** + * Callback for metrics etc + */ + onInteract: () => void +}) { + const t = useTheme() + const {_, i18n} = useLingui() + const embed = post.embed + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + + /** + * Filtering should be done at a higher level, such as `PostFeed` or + * `PostFeedVideoGridRow`, but we need to protect here as well. + */ + if (!AppBskyEmbedVideo.isView(embed)) return null + + const likeCount = post?.likeCount ?? 0 + const {thumbnail} = embed + const black = getBlackColor(t) + + return ( + { + onInteract() + }} + onPressIn={onPressIn} + onPressOut={onPressOut} + style={[ + a.flex_col, + { + alignItems: undefined, + justifyContent: undefined, + }, + ]}> + + + + + + + + + + + {_(msg`Hidden`)} + + + + + + + + + + + + + + + + + + + + + + + {likeCount > 0 && ( + + + + {formatCount(i18n, likeCount)} + + + )} + + + + + + + + ) +} + +export function CompactVideoPostCardPlaceholder() { + const t = useTheme() + const black = getBlackColor(t) + + return ( + + + + + + ) +} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index ee62da41c0..3aac27a0f8 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ScrollView,View} from 'react-native' +import {ScrollView, View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -7,19 +7,16 @@ import {useLingui} from '@lingui/react' import {VIDEO_FEED_URI} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {useTrendingSettingsApi} from '#/state/preferences/trending' -import {useSavedFeeds} from '#/state/queries/feed' import {usePostFeedQuery} from '#/state/queries/post-feed' -import {useAddSavedFeedsMutation} from '#/state/queries/preferences' -import {atoms as a, useGutters,useTheme} from '#/alf' -import {Button, ButtonIcon,ButtonText} from '#/components/Button' -import {Divider} from '#/components/Divider' -import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin' +import {atoms as a, useGutters, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import { - VideoPostCard, - VideoPostCardPlaceholder, + CompactVideoPostCard, + CompactVideoPostCardPlaceholder, } from '#/components/VideoPostCard' const CARD_WIDTH = 100 @@ -30,33 +27,9 @@ export function TrendingVideos() { const gutters = useGutters([0, 'base']) const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`) const {setTrendingVideoDisabled} = useTrendingSettingsApi() + const trendingPrompt = Prompt.usePromptControl() - const {data: saved} = useSavedFeeds() - const isSavedAlready = React.useMemo(() => { - return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI) - }, [saved]) - - const {mutateAsync: addSavedFeeds, isPending: isPinPending} = - useAddSavedFeedsMutation() - const pinFeed = React.useCallback( - (e: any) => { - e.preventDefault() - - addSavedFeeds([ - { - type: 'feed', - value: VIDEO_FEED_URI, - pinned: true, - }, - ]) - - // prevent navigation - return false - }, - [addSavedFeeds], - ) - - const hide = React.useCallback(() => { + const onConfirmHide = React.useCallback(() => { setTrendingVideoDisabled(true) logEvent('trendingVideos:hide', {context: 'interstitial'}) }, [setTrendingVideoDisabled]) @@ -69,7 +42,7 @@ export function TrendingVideos() { + onPress={() => trendingPrompt.open()}> @@ -107,7 +80,6 @@ export function TrendingVideos() { style={[ a.flex_row, a.gap_sm, - a.pb_sm, { paddingLeft: gutters.paddingLeft, paddingRight: gutters.paddingRight, @@ -118,7 +90,7 @@ export function TrendingVideos() { .fill(0) .map((_, i) => ( - + )) ) : error || !data ? ( @@ -133,8 +105,7 @@ export function TrendingVideos() { .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) .map(item => ( - - {!isSavedAlready && ( - - - - - - Pin to your home screen for easy access - - - - - )} + ) } diff --git a/src/screens/Search/components/ExploreTrendingVideos.tsx b/src/screens/Search/components/ExploreTrendingVideos.tsx new file mode 100644 index 0000000000..cdd914201e --- /dev/null +++ b/src/screens/Search/components/ExploreTrendingVideos.tsx @@ -0,0 +1,175 @@ +import React from 'react' +import {ScrollView, View} from 'react-native' +import {AppBskyEmbedVideo} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {VIDEO_FEED_URI} from '#/lib/constants' +import {logEvent} from '#/lib/statsig/statsig' +import {isWeb} from '#/platform/detection' +import {useSavedFeeds} from '#/state/queries/feed' +import {usePostFeedQuery} from '#/state/queries/post-feed' +import {useAddSavedFeedsMutation} from '#/state/queries/preferences' +import {atoms as a, tokens,useGutters, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {GradientFill} from '#/components/GradientFill' +import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin' +import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import {Text} from '#/components/Typography' +import { + CompactVideoPostCard, + CompactVideoPostCardPlaceholder, +} from '#/components/VideoPostCard' + +const CARD_WIDTH = 100 + +export function ExploreTrendingVideos() { + const t = useTheme() + const {_} = useLingui() + const gutters = useGutters([0, 'base']) + const {data, isLoading, error} = usePostFeedQuery(`feedgen|${VIDEO_FEED_URI}`) + + const {data: saved} = useSavedFeeds() + const isSavedAlready = React.useMemo(() => { + return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI) + }, [saved]) + + const {mutateAsync: addSavedFeeds, isPending: isPinPending} = + useAddSavedFeedsMutation() + const pinFeed = React.useCallback( + (e: any) => { + e.preventDefault() + + addSavedFeeds([ + { + type: 'feed', + value: VIDEO_FEED_URI, + pinned: true, + }, + ]) + + // prevent navigation + return false + }, + [addSavedFeeds], + ) + + if (error) { + return null + } + + return ( + + + + + + + Trending Videos + + + + + BETA + + + + + A new way to experience video on Bluesky + + + + + + + {isLoading ? ( + Array(10) + .fill(0) + .map((_, i) => ( + + + + )) + ) : error || !data ? ( + + Whoops! Trending videos failed to load. + + ) : ( + data.pages + .flatMap(page => page.slices) + .map(slice => slice.items[0]) + .filter(Boolean) + .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) + .map(item => ( + + { + logEvent('trendingVideo:click', {context: 'interstitial'}) + }} + /> + + )) + )} + + + + {!isSavedAlready && ( + + + + Pin the trending videos feed to your home screen for easy access + + + + + )} + + ) +} diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index e27435c35c..ecb02a7b4c 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' +import {isNative,isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -26,6 +26,7 @@ import { import {UserAvatar} from '#/view/com/util/UserAvatar' import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations' import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics' +import {ExploreTrendingVideos} from '#/screens/Search/components/ExploreTrendingVideos' import {atoms as a, useTheme, ViewStyleProp} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' @@ -246,6 +247,10 @@ type ExploreScreenItems = type: 'trendingTopics' key: string } + | { + type: 'trendingVideos' + key: string + } | { type: 'recommendations' key: string @@ -343,6 +348,13 @@ export function Explore() { key: `trending-topics`, }) + if (isNative) { + i.push({ + type: 'trendingVideos', + key: `trending-videos`, + }) + } + i.push({ type: 'recommendations', key: `recommendations`, @@ -514,6 +526,9 @@ export function Explore() { case 'trendingTopics': { return } + case 'trendingVideos': { + return + } case 'recommendations': { return } From 941bfd11cddba333bcebc7f9b122228ed207e85b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 16:20:52 -0600 Subject: [PATCH 52/59] Only use grid placeholder on native --- src/view/com/posts/PostFeed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index c9ed8a08da..55fdb69792 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -461,7 +461,7 @@ let PostFeed = ({ }) } } else { - if (isVideoFeed) { + if (isVideoFeed && isNative) { arr.push({ type: 'videoGridRowPlaceholder', key: 'videoGridRowPlaceholder', From e2512141cdbd5522ca55ed501ccab95e223227c7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 16:26:21 -0600 Subject: [PATCH 53/59] Update events --- src/components/VideoPostCard.tsx | 8 ++++---- src/components/feeds/PostFeedVideoGridRow.tsx | 4 ++++ src/components/interstitials/TrendingVideos.tsx | 6 ++++-- src/lib/statsig/events.ts | 6 +++--- src/screens/Search/components/ExploreTrendingVideos.tsx | 6 ++++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index 4b75354e17..b487b0d8ac 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -47,7 +47,7 @@ export function VideoPostCard({ /** * Callback for metrics etc */ - onInteract: () => void + onInteract?: () => void }) { const t = useTheme() const {_, i18n} = useLingui() @@ -81,7 +81,7 @@ export function VideoPostCard({ }, }} onPress={() => { - onInteract() + onInteract?.() }} onPressIn={onPressIn} onPressOut={onPressOut} @@ -334,7 +334,7 @@ export function CompactVideoPostCard({ /** * Callback for metrics etc */ - onInteract: () => void + onInteract?: () => void }) { const t = useTheme() const {_, i18n} = useLingui() @@ -366,7 +366,7 @@ export function CompactVideoPostCard({ }, }} onPress={() => { - onInteract() + onInteract?.() }} onPressIn={onPressIn} onPressOut={onPressOut} diff --git a/src/components/feeds/PostFeedVideoGridRow.tsx b/src/components/feeds/PostFeedVideoGridRow.tsx index 688fb961a1..7200afc0c1 100644 --- a/src/components/feeds/PostFeedVideoGridRow.tsx +++ b/src/components/feeds/PostFeedVideoGridRow.tsx @@ -1,6 +1,7 @@ import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' +import {logEvent} from '#/lib/statsig/statsig' import {FeedPostSliceItem} from '#/state/queries/post-feed' import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {atoms as a, useGutters} from '#/alf' @@ -41,6 +42,9 @@ export function PostFeedVideoGridRow({ post={post.post} sourceContext={sourceContext} moderation={post.moderation} + onInteract={() => { + logEvent('videoCard:click', {context: 'feed'}) + }} /> ))} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index 3aac27a0f8..d43d988ac2 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -31,7 +31,7 @@ export function TrendingVideos() { const onConfirmHide = React.useCallback(() => { setTrendingVideoDisabled(true) - logEvent('trendingVideos:hide', {context: 'interstitial'}) + logEvent('trendingVideos:hide', {context: 'interstitial:discover'}) }, [setTrendingVideoDisabled]) if (error) { @@ -113,7 +113,9 @@ export function TrendingVideos() { uri: VIDEO_FEED_URI, }} onInteract={() => { - logEvent('trendingVideo:click', {context: 'interstitial'}) + logEvent('videoCard:click', { + context: 'interstitial:discover', + }) }} /> diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index ca1f683e98..1a680c211d 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -253,10 +253,10 @@ export type LogEvents = { context: 'settings' } 'trendingVideos:hide': { - context: 'settings' | 'interstitial' | 'explore' + context: 'settings' | 'interstitial:discover' | 'interstitial:explore' } - 'trendingVideo:click': { - context: 'interstitial' | 'explore' + 'videoCard:click': { + context: 'interstitial:discover' | 'interstitial:explore' | 'feed' } 'progressGuide:hide': {} diff --git a/src/screens/Search/components/ExploreTrendingVideos.tsx b/src/screens/Search/components/ExploreTrendingVideos.tsx index cdd914201e..0bc62d7aa3 100644 --- a/src/screens/Search/components/ExploreTrendingVideos.tsx +++ b/src/screens/Search/components/ExploreTrendingVideos.tsx @@ -10,7 +10,7 @@ import {isWeb} from '#/platform/detection' import {useSavedFeeds} from '#/state/queries/feed' import {usePostFeedQuery} from '#/state/queries/post-feed' import {useAddSavedFeedsMutation} from '#/state/queries/preferences' -import {atoms as a, tokens,useGutters, useTheme} from '#/alf' +import {atoms as a, tokens, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {GradientFill} from '#/components/GradientFill' import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin' @@ -134,7 +134,9 @@ export function ExploreTrendingVideos() { uri: VIDEO_FEED_URI, }} onInteract={() => { - logEvent('trendingVideo:click', {context: 'interstitial'}) + logEvent('videoCard:click', { + context: 'interstitial:explore', + }) }} /> From c69aed8b3c7431efa3fdd9e023f53464b2d9b595 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 17 Jan 2025 16:48:40 -0600 Subject: [PATCH 54/59] Add feature gate --- src/lib/statsig/gates.ts | 1 + .../Settings/ContentAndMediaSettings.tsx | 50 +++++++++++-------- src/view/com/posts/PostFeed.tsx | 18 +++++-- src/view/screens/Search/Explore.tsx | 7 ++- 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index d2caa47f2a..7fbbe8d24e 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -7,3 +7,4 @@ export type Gate = | 'test_gate_1' | 'test_gate_2' | 'trending_topics_beta' + | 'yolo' diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index 415801370d..6ebb083b95 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -1,9 +1,11 @@ +import React from 'react' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' +import {useGate} from '#/lib/statsig/statsig' import {isNative} from '#/platform/detection' import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' import { @@ -40,6 +42,10 @@ export function ContentAndMediaSettingsScreen({}: Props) { const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() const {setTrendingDisabled, setTrendingVideoDisabled} = useTrendingSettingsApi() + const gate = useGate() + const areVideoFeedsEnabled = React.useMemo(() => { + return gate('yolo') + }, [gate]) return ( @@ -139,27 +145,29 @@ export function ContentAndMediaSettingsScreen({}: Props) { - { - const hide = Boolean(!value) - if (hide) { - logEvent('trendingVideos:hide', {context: 'settings'}) - } else { - logEvent('trendingVideos:show', {context: 'settings'}) - } - setTrendingVideoDisabled(hide) - }}> - - - - Enable trending videos - - - - + {areVideoFeedsEnabled && ( + { + const hide = Boolean(!value) + if (hide) { + logEvent('trendingVideos:hide', {context: 'settings'}) + } else { + logEvent('trendingVideos:show', {context: 'settings'}) + } + setTrendingVideoDisabled(hide) + }}> + + + + Enable trending videos + + + + + )} )} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 55fdb69792..d16ce8a0c4 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -21,6 +21,7 @@ import { } from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {useGate} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig' import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' @@ -188,7 +189,13 @@ let PostFeed = ({ const lastFetchRef = React.useRef(Date.now()) const [feedType, feedUri, feedTab] = feed.split('|') const {gtMobile, gtTablet} = useBreakpoints() - const isVideoFeed = feedUri === VIDEO_FEED_URI + const gate = useGate() + const areVideoFeedsEnabled = React.useMemo(() => { + return isNative && gate('yolo') + }, [gate]) + const isVideoFeedAndIsEnabled = React.useMemo(() => { + return feedUri === VIDEO_FEED_URI && areVideoFeedsEnabled + }, [feedUri, areVideoFeedsEnabled]) const opts = React.useMemo( () => ({enabled, ignoreFilterFor}), @@ -329,7 +336,7 @@ let PostFeed = ({ } else if (data) { let sliceIndex = -1 - if (isVideoFeed && isNative) { + if (isVideoFeedAndIsEnabled) { const rows: FeedPostSliceItem[][] = [] let slices: {slice: FeedPostSlice; index: number}[] = [] for (const page of data.pages) { @@ -386,7 +393,7 @@ let PostFeed = ({ }) } } else if (sliceIndex === 15) { - if (isNative && !trendingVideoDisabled) { + if (areVideoFeedsEnabled && !trendingVideoDisabled) { arr.push({ type: 'interstitialTrendingVideos', key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, @@ -461,7 +468,7 @@ let PostFeed = ({ }) } } else { - if (isVideoFeed && isNative) { + if (isVideoFeedAndIsEnabled) { arr.push({ type: 'videoGridRowPlaceholder', key: 'videoGridRowPlaceholder', @@ -490,7 +497,8 @@ let PostFeed = ({ trendingVideoDisabled, gtTablet, gtMobile, - isVideoFeed, + isVideoFeedAndIsEnabled, + areVideoFeedsEnabled, ]) // events diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index ecb02a7b4c..3b8386263b 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -10,9 +10,10 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useGate} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isNative,isWeb} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -308,6 +309,7 @@ export function Explore() { error: feedsError, fetchNextPage: fetchNextFeedsPage, } = useGetPopularFeedsQuery({limit: 10}) + const gate = useGate() const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles const onLoadMoreProfiles = React.useCallback(async () => { @@ -348,7 +350,7 @@ export function Explore() { key: `trending-topics`, }) - if (isNative) { + if (isNative && gate('yolo')) { i.push({ type: 'trendingVideos', key: `trending-videos`, @@ -508,6 +510,7 @@ export function Explore() { preferencesError, hasNextProfilesPage, hasNextFeedsPage, + gate, ]) const renderItem = React.useCallback( From a5b083b9568c2e5c39990fb4786824bbe578fee6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 22:59:02 +0000 Subject: [PATCH 55/59] android nav bar fixes + lower update speed --- src/screens/Feeds/VibeScreen.tsx | 43 +++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index ee587e4396..942dc92520 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -3,7 +3,6 @@ import { LayoutAnimation, ListRenderItem, ScrollView, - useWindowDimensions, View, ViewToken, } from 'react-native' @@ -21,7 +20,11 @@ import Animated, { useSharedValue, withTiming, } from 'react-native-reanimated' -import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' +import { + SafeAreaView, + useSafeAreaFrame, + useSafeAreaInsets, +} from 'react-native-safe-area-context' import {useEvent, useEventListener} from 'expo' import {Image, ImageStyle} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' @@ -64,7 +67,8 @@ import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' import {UserAvatar} from '#/view/com/util/UserAvatar' import {Header} from '#/screens/VideoFeed/Header' -import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf' +import {atoms as a, platform, ThemeProvider, tokens, useTheme} from '#/alf' +import {setNavigationBar} from '#/alf/util/navigationBar' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' @@ -75,15 +79,21 @@ import {Text} from '#/components/Typography' function createThreeVideoPlayers( sources?: [string, string, string], ): [VideoPlayer, VideoPlayer, VideoPlayer] { + // android is typically slower and can't keep up with a 0.1 interval + const eventInterval = platform({ + ios: 0.2, + android: 0.5, + default: 0, + }) const p1 = createVideoPlayer(sources?.[0] ?? '') p1.loop = true - p1.timeUpdateEventInterval = 0.25 + p1.timeUpdateEventInterval = eventInterval const p2 = createVideoPlayer(sources?.[1] ?? '') p2.loop = true - p2.timeUpdateEventInterval = 0.25 + p2.timeUpdateEventInterval = eventInterval const p3 = createVideoPlayer(sources?.[2] ?? '') p3.loop = true - p3.timeUpdateEventInterval = 0.25 + p3.timeUpdateEventInterval = eventInterval return [p1, p2, p3] } @@ -94,14 +104,17 @@ export function VideoFeed({}: NativeStackScreenProps< const {top} = useSafeAreaInsets() const {params} = useRoute>() + const t = useTheme() const setMinShellMode = useSetMinimalShellMode() useFocusEffect( useCallback(() => { setMinShellMode(true) + setNavigationBar('lightbox', t) return () => { setMinShellMode(false) + setNavigationBar('theme', t) } - }, [setMinShellMode]), + }, [setMinShellMode, t]), ) useSetLightStatusBar(true) @@ -366,7 +379,7 @@ function VideoItem({ scrollGesture: NativeGesture }) { const postShadow = usePostShadow(post) - const {height, width} = useWindowDimensions() + const {width, height} = useSafeAreaFrame() return ( @@ -593,7 +606,7 @@ function ScrubberPlaceholder() { a.w_full, { // same as Scrubber - height: bottom + tokens.space.md + tokens.space.md + tokens.space.lg, + height: bottom + tokens.space.xl, }, ]} /> @@ -607,7 +620,7 @@ function Scrubber({ player: VideoPlayer seekingAnimationSV: SharedValue }) { - const {width: screenWidth} = useWindowDimensions() + const {width: screenWidth} = useSafeAreaFrame() const insets = useSafeAreaInsets() const currentTimeSV = useSharedValue(0) const durationSV = useSharedValue(0) @@ -743,18 +756,14 @@ function Scrubber({ style={[ a.relative, a.w_full, - a.pt_lg, a.justify_end, { - paddingBottom: insets.bottom + tokens.space.md, + paddingBottom: insets.bottom, height: // bottom padding insets.bottom + - tokens.space.md + // actual height - tokens.space.md + - // top padding - tokens.space.lg, + tokens.space.xl, }, a.z_10, ]}> @@ -772,7 +781,7 @@ function ExpandableRichTextView({ value: RichTextAPI authorHandle?: string }) { - const {height: screenHeight} = useWindowDimensions() + const {height: screenHeight} = useSafeAreaFrame() const [expanded, setExpanded] = useState(false) const [constrained, setConstrained] = useState(false) const [contentHeight, setContentHeight] = useState(0) From 3b800d626d3eaaf3a738cd7aa79aa8abf60195ea Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 23:10:28 +0000 Subject: [PATCH 56/59] fix interval + decel rate on interstitials --- src/components/interstitials/TrendingVideos.tsx | 5 +++-- src/screens/Search/components/ExploreTrendingVideos.tsx | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index d43d988ac2..c0f716bce3 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -8,7 +8,7 @@ import {VIDEO_FEED_URI} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {useTrendingSettingsApi} from '#/state/preferences/trending' import {usePostFeedQuery} from '#/state/queries/post-feed' -import {atoms as a, useGutters, useTheme} from '#/alf' +import {atoms as a, tokens, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' @@ -75,7 +75,8 @@ export function TrendingVideos() { + decelerationRate="fast" + snapToInterval={CARD_WIDTH + tokens.space.sm}> + decelerationRate="fast" + snapToInterval={CARD_WIDTH + tokens.space.sm}> Date: Fri, 17 Jan 2025 23:14:05 +0000 Subject: [PATCH 57/59] attempt to fix broken scrub on android (not working) --- src/screens/Feeds/VibeScreen.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 942dc92520..81abaef5b2 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -570,7 +570,11 @@ function Overlay({ {player && active ? ( - + ) : ( )} @@ -616,9 +620,11 @@ function ScrubberPlaceholder() { function Scrubber({ player, seekingAnimationSV, + scrollGesture, }: { player: VideoPlayer seekingAnimationSV: SharedValue + scrollGesture: NativeGesture }) { const {width: screenWidth} = useSafeAreaFrame() const insets = useSafeAreaInsets() @@ -667,9 +673,15 @@ function Scrubber({ const gesture = useMemo(() => { return Gesture.Pan() + .blocksExternalGesture(scrollGesture) .failOffsetY([-10, 10]) + .onBegin(() => { + 'worklet' + console.log('begin') + }) .onStart(() => { 'worklet' + console.log('start') seekProgressSV.set(currentTimeSV.get()) isSeekingSV.set(true) seekingAnimationSV.set(withTiming(1, {duration: 500})) @@ -695,6 +707,7 @@ function Scrubber({ seekingAnimationSV.set(withTiming(0, {duration: 500})) }) }, [ + scrollGesture, seekingAnimationSV, seekBy, screenWidth, From 964acee540eeedc6b04d41864a907df440f5e3ec Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 23:58:24 +0000 Subject: [PATCH 58/59] follow button --- src/lib/statsig/events.ts | 2 + src/screens/Feeds/VibeScreen.tsx | 99 ++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 1a680c211d..af759e94e2 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -163,6 +163,7 @@ export type LogEvents = { | 'FeedInterstitial' | 'ProfileHeaderSuggestedFollows' | 'PostOnboardingFindFollows' + | 'ImmersiveVideo' } 'profile:unfollow': { logContext: @@ -179,6 +180,7 @@ export type LogEvents = { | 'FeedInterstitial' | 'ProfileHeaderSuggestedFollows' | 'PostOnboardingFindFollows' + | 'ImmersiveVideo' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 81abaef5b2..f59a0e858b 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -54,12 +54,14 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isAndroid} from '#/platform/detection' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' +import {useProfileShadow} from '#/state/cache/profile-shadow' import {usePostLikeMutationQueue} from '#/state/queries/post' import { AuthorFilter, FeedPostSliceItem, usePostFeedQuery, } from '#/state/queries/post-feed' +import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useSetMinimalShellMode} from '#/state/shell' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' @@ -69,7 +71,8 @@ import {UserAvatar} from '#/view/com/util/UserAvatar' import {Header} from '#/screens/VideoFeed/Header' import {atoms as a, platform, ThemeProvider, tokens, useTheme} from '#/alf' import {setNavigationBar} from '#/alf/util/navigationBar' -import {Button, ButtonText} from '#/components/Button' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' import {ListFooter} from '#/components/Lists' @@ -472,6 +475,12 @@ function Overlay({ const navigation = useNavigation() const seekingAnimationSV = useSharedValue(0) + const profile = useProfileShadow(post.author) + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( + profile, + 'ImmersiveVideo', + ) + const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) }) @@ -517,31 +526,73 @@ function Overlay({ colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.8)', 'rgba(0,0,0,0.95)']} style={[a.w_full, a.pt_md]}> - - - - - {sanitizeDisplayName( + + + + + + {sanitizeDisplayName( + post.author.displayName || post.author.handle, + )} + + + {sanitizeHandle(post.author.handle, '@')} + + + + {/* show button based on non-reactive version, so it doesn't hide on press */} + {!post.author.viewer?.following && ( + + )} + {record?.text?.trim() && ( Date: Fri, 17 Jan 2025 17:46:51 -0600 Subject: [PATCH 59/59] Part out the interstitials for perf, add view more --- .../interstitials/TrendingVideos.tsx | 107 ++++++++++++++---- .../components/ExploreTrendingVideos.tsx | 103 +++++++++++++---- 2 files changed, 162 insertions(+), 48 deletions(-) diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index c0f716bce3..d41371bcb7 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -1,17 +1,20 @@ import React from 'react' import {ScrollView, View} from 'react-native' -import {AppBskyEmbedVideo} from '@atproto/api' +import {AppBskyEmbedVideo, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {VIDEO_FEED_URI} from '#/lib/constants' +import {makeCustomFeedLink} from '#/lib/routes/links' import {logEvent} from '#/lib/statsig/statsig' import {useTrendingSettingsApi} from '#/state/preferences/trending' import {usePostFeedQuery} from '#/state/queries/post-feed' -import {atoms as a, tokens, useGutters, useTheme} from '#/alf' +import {atoms as a, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import {Link} from '#/components/Link' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import { @@ -76,7 +79,7 @@ export function TrendingVideos() { horizontal showsHorizontalScrollIndicator={false} decelerationRate="fast" - snapToInterval={CARD_WIDTH + tokens.space.sm}> + snapToInterval={CARD_WIDTH + a.gap_sm.gap}> Whoops! Trending videos failed to load. ) : ( - data.pages - .flatMap(page => page.slices) - .map(slice => slice.items[0]) - .filter(Boolean) - .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) - .map(item => ( - - { - logEvent('videoCard:click', { - context: 'interstitial:discover', - }) - }} - /> - - )) + )} @@ -135,3 +117,78 @@ export function TrendingVideos() { ) } + +function VideoCards({ + data, +}: { + data: Exclude['data'], undefined> +}) { + const t = useTheme() + const {_} = useLingui() + const items = React.useMemo(() => { + return data.pages + .flatMap(page => page.slices) + .map(slice => slice.items[0]) + .filter(Boolean) + .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) + .slice(0, 8) + }, [data]) + const href = React.useMemo(() => { + const urip = new AtUri(VIDEO_FEED_URI) + return makeCustomFeedLink(urip.host, urip.rkey) + }, []) + + return ( + <> + {items.map(item => ( + + { + logEvent('videoCard:click', { + context: 'interstitial:discover', + }) + }} + /> + + ))} + + + + + + View more + + + + + + + + + ) +} diff --git a/src/screens/Search/components/ExploreTrendingVideos.tsx b/src/screens/Search/components/ExploreTrendingVideos.tsx index 5a719d151f..475a6ba366 100644 --- a/src/screens/Search/components/ExploreTrendingVideos.tsx +++ b/src/screens/Search/components/ExploreTrendingVideos.tsx @@ -1,10 +1,11 @@ import React from 'react' import {ScrollView, View} from 'react-native' -import {AppBskyEmbedVideo} from '@atproto/api' +import {AppBskyEmbedVideo, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {VIDEO_FEED_URI} from '#/lib/constants' +import {makeCustomFeedLink} from '#/lib/routes/links' import {logEvent} from '#/lib/statsig/statsig' import {isWeb} from '#/platform/detection' import {useSavedFeeds} from '#/state/queries/feed' @@ -13,8 +14,10 @@ import {useAddSavedFeedsMutation} from '#/state/queries/preferences' import {atoms as a, tokens, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {GradientFill} from '#/components/GradientFill' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' +import {Link} from '#/components/Link' import {Text} from '#/components/Typography' import { CompactVideoPostCard, @@ -120,28 +123,7 @@ export function ExploreTrendingVideos() { Whoops! Trending videos failed to load. ) : ( - data.pages - .flatMap(page => page.slices) - .map(slice => slice.items[0]) - .filter(Boolean) - .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) - .map(item => ( - - { - logEvent('videoCard:click', { - context: 'interstitial:explore', - }) - }} - /> - - )) + )} @@ -176,3 +158,78 @@ export function ExploreTrendingVideos() { ) } + +function VideoCards({ + data, +}: { + data: Exclude['data'], undefined> +}) { + const t = useTheme() + const {_} = useLingui() + const items = React.useMemo(() => { + return data.pages + .flatMap(page => page.slices) + .map(slice => slice.items[0]) + .filter(Boolean) + .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) + .slice(0, 8) + }, [data]) + const href = React.useMemo(() => { + const urip = new AtUri(VIDEO_FEED_URI) + return makeCustomFeedLink(urip.host, urip.rkey) + }, []) + + return ( + <> + {items.map(item => ( + + { + logEvent('videoCard:click', { + context: 'interstitial:discover', + }) + }} + /> + + ))} + + + + + + View more + + + + + + + + + ) +}