+1
eslint/use-typed-gates.js
+1
eslint/use-typed-gates.js
+2
-2
src/lib/hooks/useOTAUpdates.ts
+2
-2
src/lib/hooks/useOTAUpdates.ts
···
31
31
}
32
32
33
33
export function useOTAUpdates() {
34
-
const shouldReceiveUpdates =
35
-
useGate('receive_updates') && isEnabled && !__DEV__
34
+
const gate = useGate()
35
+
const shouldReceiveUpdates = isEnabled && !__DEV__ && gate('receive_updates')
36
36
37
37
const appState = React.useRef<AppStateStatus>('active')
38
38
const lastMinimize = React.useRef(0)
+18
-14
src/lib/statsig/statsig.tsx
+18
-14
src/lib/statsig/statsig.tsx
···
2
2
import {Platform} from 'react-native'
3
3
import {AppState, AppStateStatus} from 'react-native'
4
4
import {sha256} from 'js-sha256'
5
-
import {
6
-
Statsig,
7
-
StatsigProvider,
8
-
useGate as useStatsigGate,
9
-
} from 'statsig-react-native-expo'
5
+
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
10
6
11
7
import {logger} from '#/logger'
12
8
import {isWeb} from '#/platform/detection'
···
98
94
}
99
95
}
100
96
101
-
export function useGate(gateName: Gate): boolean {
102
-
const {isLoading, value} = useStatsigGate(gateName)
103
-
if (isLoading) {
104
-
// This should not happen because of waitForInitialization={true}.
105
-
console.error('Did not expected isLoading to ever be true.')
97
+
export function useGate(): (gateName: Gate) => boolean {
98
+
const cache = React.useRef<Map<Gate, boolean>>()
99
+
if (cache.current === undefined) {
100
+
cache.current = new Map()
106
101
}
107
-
// This shouldn't technically be necessary but let's get a strong
108
-
// guarantee that a gate value can never change while mounted.
109
-
const [initialValue] = React.useState(value)
110
-
return initialValue
102
+
const gate = React.useCallback((gateName: Gate): boolean => {
103
+
// TODO: Replace local cache with a proper session one.
104
+
const cachedValue = cache.current!.get(gateName)
105
+
if (cachedValue !== undefined) {
106
+
return cachedValue
107
+
}
108
+
const value = Statsig.initializeCalled()
109
+
? Statsig.checkGate(gateName)
110
+
: false
111
+
cache.current!.set(gateName, value)
112
+
return value
113
+
}, [])
114
+
return gate
111
115
}
112
116
113
117
function toStatsigUser(did: string | undefined): StatsigUser {
+2
-4
src/screens/Profile/Header/ProfileHeaderStandard.tsx
+2
-4
src/screens/Profile/Header/ProfileHeaderStandard.tsx
···
80
80
})
81
81
}, [track, openModal, profile])
82
82
83
-
const autoExpandSuggestionsOnProfileFollow = useGate(
84
-
'autoexpand_suggestions_on_profile_follow',
85
-
)
83
+
const gate = useGate()
86
84
const onPressFollow = () => {
87
85
requireAuth(async () => {
88
86
try {
···
96
94
)}`,
97
95
),
98
96
)
99
-
if (isWeb && autoExpandSuggestionsOnProfileFollow) {
97
+
if (isWeb && gate('autoexpand_suggestions_on_profile_follow')) {
100
98
setShowSuggestedFollows(true)
101
99
}
102
100
} catch (e: any) {
+5
-6
src/state/shell/selected-feed.tsx
+5
-6
src/state/shell/selected-feed.tsx
···
1
1
import React from 'react'
2
2
3
+
import {Gate} from '#/lib/statsig/gates'
3
4
import {useGate} from '#/lib/statsig/statsig'
4
5
import {isWeb} from '#/platform/detection'
5
6
import * as persisted from '#/state/persisted'
···
10
11
const stateContext = React.createContext<StateContext>('home')
11
12
const setContext = React.createContext<SetContext>((_: string) => {})
12
13
13
-
function getInitialFeed(startSessionWithFollowing: boolean) {
14
+
function getInitialFeed(gate: (gateName: Gate) => boolean) {
14
15
if (isWeb) {
15
16
if (window.location.pathname === '/') {
16
17
const params = new URLSearchParams(window.location.search)
···
26
27
return feedFromSession
27
28
}
28
29
}
29
-
if (!startSessionWithFollowing) {
30
+
if (!gate('start_session_with_following')) {
30
31
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
31
32
if (feedFromPersisted) {
32
33
// Fall back to the last chosen one across all tabs.
···
37
38
}
38
39
39
40
export function Provider({children}: React.PropsWithChildren<{}>) {
40
-
const startSessionWithFollowing = useGate('start_session_with_following')
41
-
const [state, setState] = React.useState(() =>
42
-
getInitialFeed(startSessionWithFollowing),
43
-
)
41
+
const gate = useGate()
42
+
const [state, setState] = React.useState(() => getInitialFeed(gate))
44
43
45
44
const saveState = React.useCallback((feed: string) => {
46
45
setState(feed)
+4
-2
src/view/com/feeds/FeedPage.tsx
+4
-2
src/view/com/feeds/FeedPage.tsx
···
53
53
const headerOffset = useHeaderOffset()
54
54
const scrollElRef = React.useRef<ListMethods>(null)
55
55
const [hasNew, setHasNew] = React.useState(false)
56
+
const gate = useGate()
56
57
57
58
const scrollToTop = React.useCallback(() => {
58
59
scrollElRef.current?.scrollToOffset({
···
105
106
106
107
let feedPollInterval
107
108
if (
108
-
useGate('disable_poll_on_discover') &&
109
109
feed === // Discover
110
-
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
110
+
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' &&
111
+
// TODO: This gate check is still too early. Move it to where the polling happens.
112
+
gate('disable_poll_on_discover')
111
113
) {
112
114
feedPollInterval = undefined
113
115
} else {
+2
-2
src/view/com/post-thread/PostThreadFollowBtn.tsx
+2
-2
src/view/com/post-thread/PostThreadFollowBtn.tsx
···
48
48
'PostThreadItem',
49
49
)
50
50
const requireAuth = useRequireAuth()
51
-
const showFollowBackLabel = useGate('show_follow_back_label')
51
+
const gate = useGate()
52
52
53
53
const isFollowing = !!profile.viewer?.following
54
54
const isFollowedBy = !!profile.viewer?.followedBy
···
140
140
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
141
141
numberOfLines={1}>
142
142
{!isFollowing ? (
143
-
showFollowBackLabel && isFollowedBy ? (
143
+
isFollowedBy && gate('show_follow_back_label') ? (
144
144
<Trans>Follow Back</Trans>
145
145
) : (
146
146
<Trans>Follow</Trans>
+5
-3
src/view/com/util/List.tsx
+5
-3
src/view/com/util/List.tsx
···
40
40
const isScrolledDown = useSharedValue(false)
41
41
const contextScrollHandlers = useScrollHandlers()
42
42
const pal = usePalette('default')
43
-
const showsVerticalScrollIndicator =
44
-
!useGate('hide_vertical_scroll_indicators') || isWeb
43
+
const gate = useGate()
44
+
45
45
function handleScrolledDownChange(didScrollDown: boolean) {
46
46
onScrolledDownChange?.(didScrollDown)
47
47
}
···
97
97
scrollEventThrottle={1}
98
98
style={style}
99
99
ref={ref}
100
-
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
100
+
showsVerticalScrollIndicator={
101
+
isWeb || !gate('hide_vertical_scroll_indicators')
102
+
}
101
103
/>
102
104
)
103
105
}
+2
-5
src/view/com/util/Views.jsx
+2
-5
src/view/com/util/Views.jsx
···
10
10
}
11
11
12
12
export function ScrollView(props) {
13
-
const showsVerticalScrollIndicator = !useGate(
14
-
'hide_vertical_scroll_indicators',
15
-
)
16
-
13
+
const gate = useGate()
17
14
return (
18
15
<Animated.ScrollView
19
16
{...props}
20
-
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
17
+
showsVerticalScrollIndicator={!gate('hide_vertical_scroll_indicators')}
21
18
/>
22
19
)
23
20
}
+9
-10
src/view/screens/Home.tsx
+9
-10
src/view/screens/Home.tsx
···
111
111
}),
112
112
)
113
113
114
-
const disableMinShellOnForegrounding = useGate(
115
-
'disable_min_shell_on_foregrounding',
116
-
)
114
+
const gate = useGate()
117
115
React.useEffect(() => {
118
-
if (disableMinShellOnForegrounding) {
119
-
const listener = AppState.addEventListener('change', nextAppState => {
120
-
if (nextAppState === 'active') {
116
+
const listener = AppState.addEventListener('change', nextAppState => {
117
+
if (nextAppState === 'active') {
118
+
// TODO: Check if minimal shell is on before logging an exposure.
119
+
if (gate('disable_min_shell_on_foregrounding')) {
121
120
setMinimalShellMode(false)
122
121
}
123
-
})
124
-
return () => {
125
-
listener.remove()
126
122
}
123
+
})
124
+
return () => {
125
+
listener.remove()
127
126
}
128
-
}, [setMinimalShellMode, disableMinShellOnForegrounding])
127
+
}, [setMinimalShellMode, gate])
129
128
130
129
const onPageSelected = React.useCallback(
131
130
(index: number) => {
+4
-3
src/view/screens/ModerationBlockedAccounts.tsx
+4
-3
src/view/screens/ModerationBlockedAccounts.tsx
···
38
38
const setMinimalShellMode = useSetMinimalShellMode()
39
39
const {isTabletOrDesktop} = useWebMediaQueries()
40
40
const {screen} = useAnalytics()
41
-
const showsVerticalScrollIndicator =
42
-
!useGate('hide_vertical_scroll_indicators') || isWeb
41
+
const gate = useGate()
43
42
44
43
const [isPTRing, setIsPTRing] = React.useState(false)
45
44
const {
···
169
168
)}
170
169
// @ts-ignore our .web version only -prf
171
170
desktopFixedHeight
172
-
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
171
+
showsVerticalScrollIndicator={
172
+
isWeb || !gate('hide_vertical_scroll_indicators')
173
+
}
173
174
/>
174
175
)}
175
176
</CenteredView>
+5
-3
src/view/screens/ModerationMutedAccounts.tsx
+5
-3
src/view/screens/ModerationMutedAccounts.tsx
···
38
38
const setMinimalShellMode = useSetMinimalShellMode()
39
39
const {isTabletOrDesktop} = useWebMediaQueries()
40
40
const {screen} = useAnalytics()
41
-
const showsVerticalScrollIndicator =
42
-
!useGate('hide_vertical_scroll_indicators') || isWeb
41
+
const gate = useGate()
42
+
43
43
const [isPTRing, setIsPTRing] = React.useState(false)
44
44
const {
45
45
data,
···
167
167
)}
168
168
// @ts-ignore our .web version only -prf
169
169
desktopFixedHeight
170
-
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
170
+
showsVerticalScrollIndicator={
171
+
isWeb || !gate('hide_vertical_scroll_indicators')
172
+
}
171
173
/>
172
174
)}
173
175
</CenteredView>
+3
-3
src/view/screens/Profile.tsx
+3
-3
src/view/screens/Profile.tsx
···
143
143
const setMinimalShellMode = useSetMinimalShellMode()
144
144
const {openComposer} = useComposerControls()
145
145
const {screen, track} = useAnalytics()
146
-
const shouldUseScrollableHeader = useGate('new_profile_scroll_component')
146
+
const gate = useGate()
147
147
const {
148
148
data: labelerInfo,
149
149
error: labelerError,
···
317
317
// =
318
318
319
319
const renderHeader = React.useCallback(() => {
320
-
if (shouldUseScrollableHeader) {
320
+
if (gate('new_profile_scroll_component')) {
321
321
return (
322
322
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
323
323
<ProfileHeader
···
343
343
)
344
344
}
345
345
}, [
346
-
shouldUseScrollableHeader,
346
+
gate,
347
347
scrollViewTag,
348
348
profile,
349
349
labelerInfo,
+5
-5
src/view/screens/Search/Search.tsx
+5
-5
src/view/screens/Search/Search.tsx
···
210
210
211
211
function SearchScreenSuggestedFollows() {
212
212
const pal = usePalette('default')
213
-
const useSuggestedFollows = useGate('use_new_suggestions_endpoint')
213
+
const gate = useGate()
214
+
const useSuggestedFollows = gate('use_new_suggestions_endpoint')
214
215
? // Conditional hook call here is *only* OK because useGate()
215
216
// result won't change until a remount.
216
217
useSuggestedFollowsV2
···
406
407
const {isDesktop} = useWebMediaQueries()
407
408
const [activeTab, setActiveTab] = React.useState(0)
408
409
const {_} = useLingui()
409
-
410
-
const isNewSearch = useGate('new_search')
410
+
const gate = useGate()
411
411
412
412
const onPageSelected = React.useCallback(
413
413
(index: number) => {
···
420
420
421
421
const sections = React.useMemo(() => {
422
422
if (!query) return []
423
-
if (isNewSearch) {
423
+
if (gate('new_search')) {
424
424
if (hasSession) {
425
425
return [
426
426
{
···
487
487
]
488
488
}
489
489
}
490
-
}, [hasSession, isNewSearch, _, query, activeTab])
490
+
}, [hasSession, gate, _, query, activeTab])
491
491
492
492
if (hasSession) {
493
493
return query ? (