diff --git a/eslint/use-typed-gates.js b/eslint/use-typed-gates.js index 6c0331af..b245072b 100644 --- a/eslint/use-typed-gates.js +++ b/eslint/use-typed-gates.js @@ -25,6 +25,7 @@ exports.create = function create(context) { "Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.", }) } + // TODO: Verify gate() call results aren't stored in variables. }, } } diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index 70905c13..b8d331c6 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -31,8 +31,8 @@ async function setExtraParams() { } export function useOTAUpdates() { - const shouldReceiveUpdates = - useGate('receive_updates') && isEnabled && !__DEV__ + const gate = useGate() + const shouldReceiveUpdates = isEnabled && !__DEV__ && gate('receive_updates') const appState = React.useRef('active') const lastMinimize = React.useRef(0) diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index df540d79..62dd79bb 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -2,11 +2,7 @@ import React from 'react' import {Platform} from 'react-native' import {AppState, AppStateStatus} from 'react-native' import {sha256} from 'js-sha256' -import { - Statsig, - StatsigProvider, - useGate as useStatsigGate, -} from 'statsig-react-native-expo' +import {Statsig, StatsigProvider} from 'statsig-react-native-expo' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' @@ -98,16 +94,24 @@ export function logEvent( } } -export function useGate(gateName: Gate): boolean { - const {isLoading, value} = useStatsigGate(gateName) - if (isLoading) { - // This should not happen because of waitForInitialization={true}. - console.error('Did not expected isLoading to ever be true.') +export function useGate(): (gateName: Gate) => boolean { + const cache = React.useRef>() + if (cache.current === undefined) { + cache.current = new Map() } - // This shouldn't technically be necessary but let's get a strong - // guarantee that a gate value can never change while mounted. - const [initialValue] = React.useState(value) - return initialValue + const gate = React.useCallback((gateName: Gate): boolean => { + // TODO: Replace local cache with a proper session one. + const cachedValue = cache.current!.get(gateName) + if (cachedValue !== undefined) { + return cachedValue + } + const value = Statsig.initializeCalled() + ? Statsig.checkGate(gateName) + : false + cache.current!.set(gateName, value) + return value + }, []) + return gate } function toStatsigUser(did: string | undefined): StatsigUser { diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index accef12e..9e036132 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -80,9 +80,7 @@ let ProfileHeaderStandard = ({ }) }, [track, openModal, profile]) - const autoExpandSuggestionsOnProfileFollow = useGate( - 'autoexpand_suggestions_on_profile_follow', - ) + const gate = useGate() const onPressFollow = () => { requireAuth(async () => { try { @@ -96,7 +94,7 @@ let ProfileHeaderStandard = ({ )}`, ), ) - if (isWeb && autoExpandSuggestionsOnProfileFollow) { + if (isWeb && gate('autoexpand_suggestions_on_profile_follow')) { setShowSuggestedFollows(true) } } catch (e: any) { diff --git a/src/state/shell/selected-feed.tsx b/src/state/shell/selected-feed.tsx index 5c0ac0b0..dca3445f 100644 --- a/src/state/shell/selected-feed.tsx +++ b/src/state/shell/selected-feed.tsx @@ -1,5 +1,6 @@ import React from 'react' +import {Gate} from '#/lib/statsig/gates' import {useGate} from '#/lib/statsig/statsig' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' @@ -10,7 +11,7 @@ type SetContext = (v: string) => void const stateContext = React.createContext('home') const setContext = React.createContext((_: string) => {}) -function getInitialFeed(startSessionWithFollowing: boolean) { +function getInitialFeed(gate: (gateName: Gate) => boolean) { if (isWeb) { if (window.location.pathname === '/') { const params = new URLSearchParams(window.location.search) @@ -26,7 +27,7 @@ function getInitialFeed(startSessionWithFollowing: boolean) { return feedFromSession } } - if (!startSessionWithFollowing) { + if (!gate('start_session_with_following')) { const feedFromPersisted = persisted.get('lastSelectedHomeFeed') if (feedFromPersisted) { // Fall back to the last chosen one across all tabs. @@ -37,10 +38,8 @@ function getInitialFeed(startSessionWithFollowing: boolean) { } export function Provider({children}: React.PropsWithChildren<{}>) { - const startSessionWithFollowing = useGate('start_session_with_following') - const [state, setState] = React.useState(() => - getInitialFeed(startSessionWithFollowing), - ) + const gate = useGate() + const [state, setState] = React.useState(() => getInitialFeed(gate)) const saveState = React.useCallback((feed: string) => { setState(feed) diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 25c7e100..2b8fde63 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -53,6 +53,7 @@ export function FeedPage({ const headerOffset = useHeaderOffset() const scrollElRef = React.useRef(null) const [hasNew, setHasNew] = React.useState(false) + const gate = useGate() const scrollToTop = React.useCallback(() => { scrollElRef.current?.scrollToOffset({ @@ -105,9 +106,10 @@ export function FeedPage({ let feedPollInterval if ( - useGate('disable_poll_on_discover') && feed === // Discover - 'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' + 'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' && + // TODO: This gate check is still too early. Move it to where the polling happens. + gate('disable_poll_on_discover') ) { feedPollInterval = undefined } else { diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx index 8b297121..7c9a5445 100644 --- a/src/view/com/post-thread/PostThreadFollowBtn.tsx +++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx @@ -48,7 +48,7 @@ function PostThreadFollowBtnLoaded({ 'PostThreadItem', ) const requireAuth = useRequireAuth() - const showFollowBackLabel = useGate('show_follow_back_label') + const gate = useGate() const isFollowing = !!profile.viewer?.following const isFollowedBy = !!profile.viewer?.followedBy @@ -140,7 +140,7 @@ function PostThreadFollowBtnLoaded({ style={[!isFollowing ? palInverted.text : pal.text, s.bold]} numberOfLines={1}> {!isFollowing ? ( - showFollowBackLabel && isFollowedBy ? ( + isFollowedBy && gate('show_follow_back_label') ? ( Follow Back ) : ( Follow diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index b3bde2a1..5729a43a 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -40,8 +40,8 @@ function ListImpl( const isScrolledDown = useSharedValue(false) const contextScrollHandlers = useScrollHandlers() const pal = usePalette('default') - const showsVerticalScrollIndicator = - !useGate('hide_vertical_scroll_indicators') || isWeb + const gate = useGate() + function handleScrolledDownChange(didScrollDown: boolean) { onScrolledDownChange?.(didScrollDown) } @@ -97,7 +97,9 @@ function ListImpl( scrollEventThrottle={1} style={style} ref={ref} - showsVerticalScrollIndicator={showsVerticalScrollIndicator} + showsVerticalScrollIndicator={ + isWeb || !gate('hide_vertical_scroll_indicators') + } /> ) } diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx index 6850f42a..75f2b508 100644 --- a/src/view/com/util/Views.jsx +++ b/src/view/com/util/Views.jsx @@ -10,14 +10,11 @@ export function CenteredView(props) { } export function ScrollView(props) { - const showsVerticalScrollIndicator = !useGate( - 'hide_vertical_scroll_indicators', - ) - + const gate = useGate() return ( ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index b55053af..fbaa49a3 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -111,21 +111,20 @@ function HomeScreenReady({ }), ) - const disableMinShellOnForegrounding = useGate( - 'disable_min_shell_on_foregrounding', - ) + const gate = useGate() React.useEffect(() => { - if (disableMinShellOnForegrounding) { - const listener = AppState.addEventListener('change', nextAppState => { - if (nextAppState === 'active') { + const listener = AppState.addEventListener('change', nextAppState => { + if (nextAppState === 'active') { + // TODO: Check if minimal shell is on before logging an exposure. + if (gate('disable_min_shell_on_foregrounding')) { setMinimalShellMode(false) } - }) - return () => { - listener.remove() } + }) + return () => { + listener.remove() } - }, [setMinimalShellMode, disableMinShellOnForegrounding]) + }, [setMinimalShellMode, gate]) const onPageSelected = React.useCallback( (index: number) => { diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index 7b68c225..b7ce8cdd 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -38,8 +38,7 @@ export function ModerationBlockedAccounts({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() - const showsVerticalScrollIndicator = - !useGate('hide_vertical_scroll_indicators') || isWeb + const gate = useGate() const [isPTRing, setIsPTRing] = React.useState(false) const { @@ -169,7 +168,9 @@ export function ModerationBlockedAccounts({}: Props) { )} // @ts-ignore our .web version only -prf desktopFixedHeight - showsVerticalScrollIndicator={showsVerticalScrollIndicator} + showsVerticalScrollIndicator={ + isWeb || !gate('hide_vertical_scroll_indicators') + } /> )} diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index 22dd5a27..4d7ca629 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -38,8 +38,8 @@ export function ModerationMutedAccounts({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() - const showsVerticalScrollIndicator = - !useGate('hide_vertical_scroll_indicators') || isWeb + const gate = useGate() + const [isPTRing, setIsPTRing] = React.useState(false) const { data, @@ -167,7 +167,9 @@ export function ModerationMutedAccounts({}: Props) { )} // @ts-ignore our .web version only -prf desktopFixedHeight - showsVerticalScrollIndicator={showsVerticalScrollIndicator} + showsVerticalScrollIndicator={ + isWeb || !gate('hide_vertical_scroll_indicators') + } /> )} diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index f71e1330..c7f5a662 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -143,7 +143,7 @@ function ProfileScreenLoaded({ const setMinimalShellMode = useSetMinimalShellMode() const {openComposer} = useComposerControls() const {screen, track} = useAnalytics() - const shouldUseScrollableHeader = useGate('new_profile_scroll_component') + const gate = useGate() const { data: labelerInfo, error: labelerError, @@ -317,7 +317,7 @@ function ProfileScreenLoaded({ // = const renderHeader = React.useCallback(() => { - if (shouldUseScrollableHeader) { + if (gate('new_profile_scroll_component')) { return ( { @@ -420,7 +420,7 @@ export function SearchScreenInner({ const sections = React.useMemo(() => { if (!query) return [] - if (isNewSearch) { + if (gate('new_search')) { if (hasSession) { return [ { @@ -487,7 +487,7 @@ export function SearchScreenInner({ ] } } - }, [hasSession, isNewSearch, _, query, activeTab]) + }, [hasSession, gate, _, query, activeTab]) if (hasSession) { return query ? (