From bc39b553f97fb891974148ad730996cea9f07f37 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Thu, 4 Dec 2025 15:39:56 -0800 Subject: [PATCH] Adds a composer prompt on the home screen to invite users to post (#9464) * Add a composer prompt on home * Added image upload button * Feed composer buttons * Feed composer styles * Add client events, auto-open gallery when button is pressed * Add feature gate * Move gate check to last * Small style cleanups --------- Co-authored-by: Eric Bailey --- src/lib/statsig/gates.ts | 1 + src/logger/metrics.ts | 4 + src/state/shell/composer/index.tsx | 1 + src/view/com/composer/Composer.tsx | 5 + src/view/com/composer/SelectMediaButton.tsx | 15 +- src/view/com/feeds/ComposerPrompt.tsx | 247 ++++++++++++++++++++ src/view/com/posts/PostFeed.tsx | 34 ++- src/view/shell/Composer.ios.tsx | 1 + src/view/shell/Composer.tsx | 1 + src/view/shell/Composer.web.tsx | 1 + 10 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/view/com/feeds/ComposerPrompt.tsx diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index aa839542f2..ea263e22e5 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -9,5 +9,6 @@ export type Gate = | 'onboarding_add_video_feed' | 'onboarding_suggested_starterpacks' | 'remove_show_latest_button' + | 'show_composer_prompt' | 'test_gate_1' | 'test_gate_2' diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 57b03169d0..3903354b7e 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -203,6 +203,10 @@ export type MetricEvents = { 'composer:gif:open': {} 'composer:gif:select': {} + 'postComposer:click': {} + 'composerPrompt:press': {} + 'composerPrompt:camera:press': {} + 'composerPrompt:gallery:press': {} 'composer:threadgate:open': { nudged: boolean diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx index 1d0973add1..8449847770 100644 --- a/src/state/shell/composer/index.tsx +++ b/src/state/shell/composer/index.tsx @@ -43,6 +43,7 @@ export interface ComposerOpts { text?: string imageUris?: {uri: string; width: number; height: number; altText?: string}[] videoUri?: {uri: string; width: number; height: number} + openGallery?: boolean } type StateContext = ComposerOpts | undefined diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 7746d93c30..2b2eaed64e 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -172,6 +172,7 @@ export const ComposePost = ({ text: initText, imageUris: initImageUris, videoUri: initVideoUri, + openGallery, cancelRef, }: Props & { cancelRef?: React.RefObject @@ -721,6 +722,7 @@ export const ComposePost = ({ }} currentLanguages={currentLanguages} onSelectLanguage={onSelectLanguage} + openGallery={openGallery} /> ) @@ -1334,6 +1336,7 @@ function ComposerFooter({ onAddPost, currentLanguages, onSelectLanguage, + openGallery, }: { post: PostDraft dispatch: (action: PostAction) => void @@ -1344,6 +1347,7 @@ function ComposerFooter({ onAddPost: () => void currentLanguages: string[] onSelectLanguage?: (language: string) => void + openGallery?: boolean }) { const t = useTheme() const {_} = useLingui() @@ -1463,6 +1467,7 @@ function ComposerFooter({ allowedAssetTypes={selectedAssetsType} selectedAssetsCount={selectedAssetsCount} onSelectAssets={onSelectAssets} + autoOpen={openGallery} /> void + /** + * If true, automatically open the media picker when the component mounts. + */ + autoOpen?: boolean } /** @@ -358,12 +362,14 @@ export function SelectMediaButton({ allowedAssetTypes, selectedAssetsCount, onSelectAssets, + autoOpen, }: SelectMediaButtonProps) { const {_} = useLingui() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() const sheetWrapper = useSheetWrapper() const t = useTheme() + const hasAutoOpened = useRef(false) const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount @@ -460,6 +466,13 @@ export function SelectMediaButton({ selectionCountRemaining, ]) + useEffect(() => { + if (autoOpen && !hasAutoOpened.current && !disabled) { + hasAutoOpened.current = true + onPressSelectMedia() + } + }, [autoOpen, disabled, onPressSelectMedia]) + return ( + )} + + + + + + ) +} diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 7e70d918c3..4f25468c95 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -31,7 +31,7 @@ import {isStatusStillActive, validateStatus} from '#/lib/actor-status' import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {logEvent} from '#/lib/statsig/statsig' +import {logEvent, useGate} from '#/lib/statsig/statsig' import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {isIOS, isNative, isWeb} from '#/platform/detection' @@ -70,6 +70,7 @@ import { } from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' +import {ComposerPrompt} from '../feeds/ComposerPrompt' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' import {PostFeedErrorMessage} from './PostFeedErrorMessage' @@ -150,6 +151,10 @@ type FeedRow = type: 'ageAssuranceBanner' key: string } + | { + type: 'composerPrompt' + key: string + } export function getItemsForFeedback(feedRow: FeedRow): { item: FeedPostSliceItem @@ -225,6 +230,7 @@ let PostFeed = ({ const {_} = useLingui() const queryClient = useQueryClient() const {currentAccount, hasSession} = useSession() + const gate = useGate() const initialNumToRender = useInitialNumToRender() const feedFeedback = useFeedFeedbackContext() const [isPTRing, setIsPTRing] = useState(false) @@ -511,6 +517,18 @@ let PostFeed = ({ 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, }) } + // Show composer prompt for Discover and Following feeds + if ( + hasSession && + (feedUriOrActorDid === DISCOVER_FEED_URI || + feed === 'following') && + gate('show_composer_prompt') + ) { + arr.push({ + type: 'composerPrompt', + key: 'composerPrompt-' + sliceIndex, + }) + } } else if (sliceIndex === 15) { if (areVideoFeedsEnabled && !trendingVideoDisabled) { arr.push({ @@ -524,6 +542,16 @@ let PostFeed = ({ key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, }) } + } else if (feedKind === 'following') { + if (sliceIndex === 0) { + // Show composer prompt for Following feed + if (hasSession && gate('show_composer_prompt')) { + arr.push({ + type: 'composerPrompt', + key: 'composerPrompt-' + sliceIndex, + }) + } + } } else if (feedKind === 'profile') { if (sliceIndex === 5) { arr.push({ @@ -638,6 +666,7 @@ let PostFeed = ({ isEmpty, lastFetchedAt, data, + feed, feedType, feedUriOrActorDid, feedTab, @@ -652,6 +681,7 @@ let PostFeed = ({ hasPressedShowLessUris, ageAssuranceBannerState, isCurrentFeedAtStartupSelected, + gate, blockedOrMutedAuthors, ]) @@ -743,6 +773,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'composerPrompt') { + return } else if (row.type === 'interstitialTrendingVideos') { return } else if (row.type === 'fallbackMarker') { diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 8a0ee60055..3e4a551f0b 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -45,6 +45,7 @@ export function Composer({}: {winHeight: number}) { text={state?.text} imageUris={state?.imageUris} videoUri={state?.videoUri} + openGallery={state?.openGallery} /> diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index a17de6163d..a01cc9744d 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -55,6 +55,7 @@ export function Composer({winHeight}: {winHeight: number}) { text={state.text} imageUris={state.imageUris} videoUri={state.videoUri} + openGallery={state.openGallery} /> ) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index a27e891680..df33ebcd4d 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -110,6 +110,7 @@ function Inner({state}: {state: ComposerOpts}) { openEmojiPicker={onOpenPicker} text={state.text} imageUris={state.imageUris} + openGallery={state.openGallery} />