[APP-1782] Analytics migration (#9734)
* WIP * Clean up growthbook code, integrate into init and sessions * Move everything out of React * Add metrics client * Move to separate file * Shared metadata cache * Ensure we update metadata when session ID changes * Ensure userMetadata is cleared when logging out * WIP revamp * Integrate feature gates into analytics context * Clean up old code * Fix useMeta util * Some comments and cleanup * Add logger to base analytics context * Refactor current route handling * Rip out LogEvent from navigation * Update tracking endpoint * Migrate toClout * Clear out statsig client * Add todo, reset logger readme * Ope fix statsig noop * Refactor logging in feed-feedback, add debug logging to metrics client * Remove LogEvents alias for Metrics * Prefer root package export * Remove Metrics alias from logger * [APP-1782] Migrate to new analytics APIs (#9735) * Migrate logEvent to useAnalytics * Migrate logger.metric to useAnalytics * Migrate tricky spot, fix types * Migrate remaining tricky spot * Missed one * Remove metric() from logger * Migrate useGate to useAnalytics * Remove all other StatSig mentions * Update event payload * Update logger tests * Mock expo method * Fix session ID bug * Add session ID test * Add test for metrics client * Clarify intent * Clean up core analytics file * Clean up the call once utils * Fix TODO * Fix TODO * Fix TODO * Fix TODO * Fix TODO * Remove debug code * Fix navigation context * OK nav context is not working, todo * Checkpoint: works but feels hacky * Fix navigation context issue * Improve feature API * Improve metric logging * Update logger tests
This commit is contained in:
@@ -28,6 +28,13 @@ EXPO_PUBLIC_CHAT_PROXY_DID=
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Bluesky's metrics API
|
||||||
|
EXPO_PUBLIC_METRICS_API_HOST=
|
||||||
|
|
||||||
|
# Growthbook config
|
||||||
|
EXPO_PUBLIC_GROWTHBOOK_API_HOST=
|
||||||
|
EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY=
|
||||||
|
|
||||||
# Sentry DSN for telemetry
|
# Sentry DSN for telemetry
|
||||||
EXPO_PUBLIC_SENTRY_DSN=
|
EXPO_PUBLIC_SENTRY_DSN=
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export default defineConfig(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'bsky-internal/use-exact-imports': 'error',
|
'bsky-internal/use-exact-imports': 'error',
|
||||||
'bsky-internal/use-typed-gates': 'error',
|
|
||||||
'bsky-internal/use-prefixed-imports': 'error',
|
'bsky-internal/use-prefixed-imports': 'error',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const plugin = {
|
|||||||
rules: {
|
rules: {
|
||||||
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
|
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
|
||||||
'use-exact-imports': require('./use-exact-imports'),
|
'use-exact-imports': require('./use-exact-imports'),
|
||||||
'use-typed-gates': require('./use-typed-gates'),
|
|
||||||
'use-prefixed-imports': require('./use-prefixed-imports'),
|
'use-prefixed-imports': require('./use-prefixed-imports'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
meta: {
|
|
||||||
type: 'suggestion',
|
|
||||||
docs: {
|
|
||||||
description:
|
|
||||||
'Enforce using internal statsig wrapper instead of npm package',
|
|
||||||
},
|
|
||||||
schema: [],
|
|
||||||
},
|
|
||||||
create(context) {
|
|
||||||
return {
|
|
||||||
ImportSpecifier(node) {
|
|
||||||
if (
|
|
||||||
!node.local ||
|
|
||||||
node.local.type !== 'Identifier' ||
|
|
||||||
node.local.name !== 'useGate'
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
node.parent.type !== 'ImportDeclaration' ||
|
|
||||||
!node.parent.source ||
|
|
||||||
node.parent.source.type !== 'Literal'
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const source = node.parent.source.value
|
|
||||||
if (source.startsWith('statsig') || source.startsWith('@statsig')) {
|
|
||||||
context.report({
|
|
||||||
node,
|
|
||||||
message:
|
|
||||||
"Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// TODO: Verify gate() call results aren't stored in variables.
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
+1
-11
@@ -99,19 +99,9 @@ jest.mock('expo-modules-core', () => ({
|
|||||||
requireNativeViewManager: jest.fn().mockImplementation(_ => {
|
requireNativeViewManager: jest.fn().mockImplementation(_ => {
|
||||||
return () => null
|
return () => null
|
||||||
}),
|
}),
|
||||||
|
createPermissionHook: () => () => [true],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('expo-localization', () => ({
|
jest.mock('expo-localization', () => ({
|
||||||
getLocales: () => [],
|
getLocales: () => [],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('statsig-react-native-expo', () => ({
|
|
||||||
Statsig: {
|
|
||||||
initialize() {},
|
|
||||||
initializeCalled() {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('../src/lib/statsig/statsig', () => ({}))
|
|
||||||
|
|||||||
+1
-1
@@ -93,6 +93,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||||
|
"@growthbook/growthbook-react": "^1.6.2",
|
||||||
"@haileyok/bluesky-video": "0.3.2",
|
"@haileyok/bluesky-video": "0.3.2",
|
||||||
"@ipld/dag-cbor": "^9.2.0",
|
"@ipld/dag-cbor": "^9.2.0",
|
||||||
"@lingui/react": "^4.14.1",
|
"@lingui/react": "^4.14.1",
|
||||||
@@ -219,7 +220,6 @@
|
|||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sonner-native": "^0.21.0",
|
"sonner-native": "^0.21.0",
|
||||||
"statsig-react-native-expo": "^4.6.1",
|
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"tldts": "^6.1.46",
|
"tldts": "^6.1.46",
|
||||||
|
|||||||
+40
-33
@@ -17,7 +17,6 @@ import * as Sentry from '@sentry/react-native'
|
|||||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||||
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
||||||
import {QueryProvider} from '#/lib/react-query'
|
import {QueryProvider} from '#/lib/react-query'
|
||||||
import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig'
|
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||||
import I18nProvider from '#/locale/i18nProvider'
|
import I18nProvider from '#/locale/i18nProvider'
|
||||||
@@ -69,6 +68,12 @@ import {
|
|||||||
prefetchAgeAssuranceConfig,
|
prefetchAgeAssuranceConfig,
|
||||||
Provider as AgeAssuranceV2Provider,
|
Provider as AgeAssuranceV2Provider,
|
||||||
} from '#/ageAssurance'
|
} from '#/ageAssurance'
|
||||||
|
import {
|
||||||
|
AnalyticsContext,
|
||||||
|
AnalyticsFeaturesContext,
|
||||||
|
features,
|
||||||
|
setupDeviceId,
|
||||||
|
} from '#/analytics'
|
||||||
import {IS_ANDROID, IS_IOS} from '#/env'
|
import {IS_ANDROID, IS_IOS} from '#/env'
|
||||||
import {
|
import {
|
||||||
prefetchLiveEvents,
|
prefetchLiveEvents,
|
||||||
@@ -114,7 +119,7 @@ function InnerApp() {
|
|||||||
if (account) {
|
if (account) {
|
||||||
await resumeSession(account)
|
await resumeSession(account)
|
||||||
} else {
|
} else {
|
||||||
await tryFetchGates(undefined, 'prefer-fresh-gates')
|
await features.init
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`session: resume failed`, {message: e})
|
logger.error(`session: resume failed`, {message: e})
|
||||||
@@ -144,9 +149,9 @@ function InnerApp() {
|
|||||||
<React.Fragment
|
<React.Fragment
|
||||||
// Resets the entire tree below when it changes:
|
// Resets the entire tree below when it changes:
|
||||||
key={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<AnalyticsFeaturesContext>
|
||||||
<PolicyUpdateOverlayProvider>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<PolicyUpdateOverlayProvider>
|
||||||
<LiveEventsProvider>
|
<LiveEventsProvider>
|
||||||
<AgeAssuranceV2Provider>
|
<AgeAssuranceV2Provider>
|
||||||
<ComposerProvider>
|
<ComposerProvider>
|
||||||
@@ -192,9 +197,9 @@ function InnerApp() {
|
|||||||
</ComposerProvider>
|
</ComposerProvider>
|
||||||
</AgeAssuranceV2Provider>
|
</AgeAssuranceV2Provider>
|
||||||
</LiveEventsProvider>
|
</LiveEventsProvider>
|
||||||
</StatsigProvider>
|
</PolicyUpdateOverlayProvider>
|
||||||
</PolicyUpdateOverlayProvider>
|
</QueryProvider>
|
||||||
</QueryProvider>
|
</AnalyticsFeaturesContext>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
</VideoVolumeProvider>
|
</VideoVolumeProvider>
|
||||||
</Splash>
|
</Splash>
|
||||||
@@ -208,7 +213,7 @@ function App() {
|
|||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
Promise.all([initPersistedState(), Geo.resolve()]).then(() =>
|
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||||
setReady(true),
|
setReady(true),
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -226,30 +231,32 @@ function App() {
|
|||||||
<A11yProvider>
|
<A11yProvider>
|
||||||
<KeyboardControllerProvider>
|
<KeyboardControllerProvider>
|
||||||
<OnboardingProvider>
|
<OnboardingProvider>
|
||||||
<SessionProvider>
|
<AnalyticsContext>
|
||||||
<PrefsStateProvider>
|
<SessionProvider>
|
||||||
<I18nProvider>
|
<PrefsStateProvider>
|
||||||
<ShellStateProvider>
|
<I18nProvider>
|
||||||
<ModalStateProvider>
|
<ShellStateProvider>
|
||||||
<DialogStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<DialogStateProvider>
|
||||||
<PortalProvider>
|
<LightboxStateProvider>
|
||||||
<BottomSheetProvider>
|
<PortalProvider>
|
||||||
<StarterPackProvider>
|
<BottomSheetProvider>
|
||||||
<SafeAreaProvider
|
<StarterPackProvider>
|
||||||
initialMetrics={initialWindowMetrics}>
|
<SafeAreaProvider
|
||||||
<InnerApp />
|
initialMetrics={initialWindowMetrics}>
|
||||||
</SafeAreaProvider>
|
<InnerApp />
|
||||||
</StarterPackProvider>
|
</SafeAreaProvider>
|
||||||
</BottomSheetProvider>
|
</StarterPackProvider>
|
||||||
</PortalProvider>
|
</BottomSheetProvider>
|
||||||
</LightboxStateProvider>
|
</PortalProvider>
|
||||||
</DialogStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</DialogStateProvider>
|
||||||
</ShellStateProvider>
|
</ModalStateProvider>
|
||||||
</I18nProvider>
|
</ShellStateProvider>
|
||||||
</PrefsStateProvider>
|
</I18nProvider>
|
||||||
</SessionProvider>
|
</PrefsStateProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</AnalyticsContext>
|
||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
</KeyboardControllerProvider>
|
</KeyboardControllerProvider>
|
||||||
</A11yProvider>
|
</A11yProvider>
|
||||||
|
|||||||
+40
-29
@@ -9,7 +9,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import * as Sentry from '@sentry/react-native'
|
import * as Sentry from '@sentry/react-native'
|
||||||
|
|
||||||
import {QueryProvider} from '#/lib/react-query'
|
import {QueryProvider} from '#/lib/react-query'
|
||||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
|
||||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||||
import I18nProvider from '#/locale/i18nProvider'
|
import I18nProvider from '#/locale/i18nProvider'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -55,8 +54,16 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
|||||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
import {ToastOutlet} from '#/components/Toast'
|
import {ToastOutlet} from '#/components/Toast'
|
||||||
import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance'
|
import {
|
||||||
import {prefetchAgeAssuranceConfig} from '#/ageAssurance'
|
prefetchAgeAssuranceConfig,
|
||||||
|
Provider as AgeAssuranceV2Provider,
|
||||||
|
} from '#/ageAssurance'
|
||||||
|
import {
|
||||||
|
AnalyticsContext,
|
||||||
|
AnalyticsFeaturesContext,
|
||||||
|
features,
|
||||||
|
setupDeviceId,
|
||||||
|
} from '#/analytics'
|
||||||
import {
|
import {
|
||||||
prefetchLiveEvents,
|
prefetchLiveEvents,
|
||||||
Provider as LiveEventsProvider,
|
Provider as LiveEventsProvider,
|
||||||
@@ -87,6 +94,8 @@ function InnerApp() {
|
|||||||
try {
|
try {
|
||||||
if (account) {
|
if (account) {
|
||||||
await resumeSession(account)
|
await resumeSession(account)
|
||||||
|
} else {
|
||||||
|
await features.init
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`session: resumeSession failed`, {message: e})
|
logger.error(`session: resumeSession failed`, {message: e})
|
||||||
@@ -119,9 +128,9 @@ function InnerApp() {
|
|||||||
<React.Fragment
|
<React.Fragment
|
||||||
// Resets the entire tree below when it changes:
|
// Resets the entire tree below when it changes:
|
||||||
key={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<AnalyticsFeaturesContext>
|
||||||
<PolicyUpdateOverlayProvider>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<PolicyUpdateOverlayProvider>
|
||||||
<LiveEventsProvider>
|
<LiveEventsProvider>
|
||||||
<AgeAssuranceV2Provider>
|
<AgeAssuranceV2Provider>
|
||||||
<ComposerProvider>
|
<ComposerProvider>
|
||||||
@@ -163,9 +172,9 @@ function InnerApp() {
|
|||||||
</ComposerProvider>
|
</ComposerProvider>
|
||||||
</AgeAssuranceV2Provider>
|
</AgeAssuranceV2Provider>
|
||||||
</LiveEventsProvider>
|
</LiveEventsProvider>
|
||||||
</StatsigProvider>
|
</PolicyUpdateOverlayProvider>
|
||||||
</PolicyUpdateOverlayProvider>
|
</QueryProvider>
|
||||||
</QueryProvider>
|
</AnalyticsFeaturesContext>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
</ActiveVideoProvider>
|
</ActiveVideoProvider>
|
||||||
</VideoVolumeProvider>
|
</VideoVolumeProvider>
|
||||||
@@ -179,7 +188,7 @@ function App() {
|
|||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
Promise.all([initPersistedState(), Geo.resolve()]).then(() =>
|
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||||
setReady(true),
|
setReady(true),
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -196,25 +205,27 @@ function App() {
|
|||||||
<Geo.Provider>
|
<Geo.Provider>
|
||||||
<A11yProvider>
|
<A11yProvider>
|
||||||
<OnboardingProvider>
|
<OnboardingProvider>
|
||||||
<SessionProvider>
|
<AnalyticsContext>
|
||||||
<PrefsStateProvider>
|
<SessionProvider>
|
||||||
<I18nProvider>
|
<PrefsStateProvider>
|
||||||
<ShellStateProvider>
|
<I18nProvider>
|
||||||
<ModalStateProvider>
|
<ShellStateProvider>
|
||||||
<DialogStateProvider>
|
<ModalStateProvider>
|
||||||
<LightboxStateProvider>
|
<DialogStateProvider>
|
||||||
<PortalProvider>
|
<LightboxStateProvider>
|
||||||
<StarterPackProvider>
|
<PortalProvider>
|
||||||
<InnerApp />
|
<StarterPackProvider>
|
||||||
</StarterPackProvider>
|
<InnerApp />
|
||||||
</PortalProvider>
|
</StarterPackProvider>
|
||||||
</LightboxStateProvider>
|
</PortalProvider>
|
||||||
</DialogStateProvider>
|
</LightboxStateProvider>
|
||||||
</ModalStateProvider>
|
</DialogStateProvider>
|
||||||
</ShellStateProvider>
|
</ModalStateProvider>
|
||||||
</I18nProvider>
|
</ShellStateProvider>
|
||||||
</PrefsStateProvider>
|
</I18nProvider>
|
||||||
</SessionProvider>
|
</PrefsStateProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</AnalyticsContext>
|
||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
</A11yProvider>
|
</A11yProvider>
|
||||||
</Geo.Provider>
|
</Geo.Provider>
|
||||||
|
|||||||
+67
-81
@@ -28,7 +28,7 @@ import {
|
|||||||
storePayloadForAccountSwitch,
|
storePayloadForAccountSwitch,
|
||||||
} from '#/lib/hooks/useNotificationHandler'
|
} from '#/lib/hooks/useNotificationHandler'
|
||||||
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
||||||
import {logger as notyLogger} from '#/lib/notifications/util'
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {buildStateObject} from '#/lib/routes/helpers'
|
import {buildStateObject} from '#/lib/routes/helpers'
|
||||||
import {
|
import {
|
||||||
type AllNavigatorParams,
|
type AllNavigatorParams,
|
||||||
@@ -38,12 +38,11 @@ import {
|
|||||||
type MessagesTabNavigatorParams,
|
type MessagesTabNavigatorParams,
|
||||||
type MyProfileTabNavigatorParams,
|
type MyProfileTabNavigatorParams,
|
||||||
type NotificationsTabNavigatorParams,
|
type NotificationsTabNavigatorParams,
|
||||||
|
type RouteParams,
|
||||||
type SearchTabNavigatorParams,
|
type SearchTabNavigatorParams,
|
||||||
|
type State,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {type RouteParams, type State} from '#/lib/routes/types'
|
|
||||||
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {bskyTitle} from '#/lib/strings/headings'
|
import {bskyTitle} from '#/lib/strings/headings'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
@@ -137,6 +136,8 @@ import {
|
|||||||
EmailDialogScreenID,
|
EmailDialogScreenID,
|
||||||
useEmailDialogControl,
|
useEmailDialogControl,
|
||||||
} from '#/components/dialogs/EmailDialog'
|
} from '#/components/dialogs/EmailDialog'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {setNavigationMetadata} from '#/analytics/metadata'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
@@ -879,11 +880,13 @@ const LINKING = {
|
|||||||
let lastHandledNotificationDateDedupe: number | undefined
|
let lastHandledNotificationDateDedupe: number | undefined
|
||||||
|
|
||||||
function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const notyLogger = ax.logger.useChild(ax.logger.Context.Notifications)
|
||||||
const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
|
const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
|
||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
const {onPressSwitchAccount} = useAccountSwitcher()
|
const {onPressSwitchAccount} = useAccountSwitcher()
|
||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
const prevLoggedRouteName = useRef<string | undefined>(undefined)
|
const previousScreen = useRef<string | undefined>(undefined)
|
||||||
const emailDialogControl = useEmailDialogControl()
|
const emailDialogControl = useEmailDialogControl()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
|
|
||||||
@@ -945,11 +948,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
const payload = getNotificationPayload(response.notification)
|
const payload = getNotificationPayload(response.notification)
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
notyLogger.metric(
|
ax.metric('notifications:openApp', {
|
||||||
'notifications:openApp',
|
reason: payload.reason,
|
||||||
{reason: payload.reason, causedBoot: true},
|
causedBoot: true,
|
||||||
{statsig: false},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (payload.reason === 'chat-message') {
|
if (payload.reason === 'chat-message') {
|
||||||
handleChatMessage(payload)
|
handleChatMessage(payload)
|
||||||
@@ -973,47 +975,69 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReady() {
|
const onNavigationReady = useCallOnce(() => {
|
||||||
prevLoggedRouteName.current = getCurrentRouteName()
|
const currentScreen = getCurrentRouteName()
|
||||||
|
setNavigationMetadata({
|
||||||
|
previousScreen: currentScreen,
|
||||||
|
currentScreen,
|
||||||
|
})
|
||||||
|
previousScreen.current = currentScreen
|
||||||
|
|
||||||
|
handlePushNotificationEntry()
|
||||||
|
|
||||||
|
ax.metric('router:navigate', {})
|
||||||
|
|
||||||
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
|
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
|
||||||
emailDialogControl.open({
|
emailDialogControl.open({
|
||||||
id: EmailDialogScreenID.VerificationReminder,
|
id: EmailDialogScreenID.VerificationReminder,
|
||||||
})
|
})
|
||||||
snoozeEmailConfirmationPrompt()
|
snoozeEmailConfirmationPrompt()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
ax.metric('init', {
|
||||||
|
initMs: Math.round(
|
||||||
|
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||||
|
performance.now() - global.__BUNDLE_START_TIME__,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (IS_WEB) {
|
||||||
|
const referrerInfo = Referrer.getReferrerInfo()
|
||||||
|
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||||
|
ax.metric('deepLink:referrerReceived', {
|
||||||
|
to: window.location.href,
|
||||||
|
referrer: referrerInfo?.referrer,
|
||||||
|
hostname: referrerInfo?.hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<NavigationContainer
|
||||||
<NavigationContainer
|
ref={navigationRef}
|
||||||
ref={navigationRef}
|
linking={LINKING}
|
||||||
linking={LINKING}
|
theme={theme}
|
||||||
theme={theme}
|
onStateChange={() => {
|
||||||
onStateChange={() => {
|
const currentScreen = getCurrentRouteName()
|
||||||
logger.metric(
|
// do this before metric
|
||||||
'router:navigate',
|
setNavigationMetadata({
|
||||||
{from: prevLoggedRouteName.current},
|
previousScreen: previousScreen.current,
|
||||||
{statsig: false},
|
currentScreen,
|
||||||
)
|
})
|
||||||
prevLoggedRouteName.current = getCurrentRouteName()
|
ax.metric('router:navigate', {from: previousScreen.current})
|
||||||
}}
|
previousScreen.current = currentScreen
|
||||||
onReady={() => {
|
}}
|
||||||
attachRouteToLogEvents(getCurrentRouteName)
|
onReady={onNavigationReady}
|
||||||
logModuleInitTime()
|
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
||||||
onReady()
|
// However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
|
||||||
logger.metric('router:navigate', {}, {statsig: false})
|
// See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly.
|
||||||
handlePushNotificationEntry()
|
// I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now.
|
||||||
}}
|
// We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
|
||||||
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
// -sfn
|
||||||
// However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
|
navigationInChildEnabled>
|
||||||
// See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly.
|
{children}
|
||||||
// I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now.
|
</NavigationContainer>
|
||||||
// We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
|
|
||||||
// -sfn
|
|
||||||
navigationInChildEnabled>
|
|
||||||
{children}
|
|
||||||
</NavigationContainer>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,44 +1111,6 @@ function reset(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let didInit = false
|
|
||||||
function logModuleInitTime() {
|
|
||||||
if (didInit) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
didInit = true
|
|
||||||
|
|
||||||
const initMs = Math.round(
|
|
||||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
|
||||||
performance.now() - global.__BUNDLE_START_TIME__,
|
|
||||||
)
|
|
||||||
console.log(`Time to first paint: ${initMs} ms`)
|
|
||||||
logEvent('init', {
|
|
||||||
initMs,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (IS_WEB) {
|
|
||||||
const referrerInfo = Referrer.getReferrerInfo()
|
|
||||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
|
||||||
logEvent('deepLink:referrerReceived', {
|
|
||||||
to: window.location.href,
|
|
||||||
referrer: referrerInfo?.referrer,
|
|
||||||
hostname: referrerInfo?.hostname,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__DEV__) {
|
|
||||||
// This log is noisy, so keep false committed
|
|
||||||
const shouldLog = false
|
|
||||||
// Relies on our patch to polyfill.js in metro-runtime
|
|
||||||
const initLogs = (global as any).__INIT_LOGS__
|
|
||||||
if (shouldLog && Array.isArray(initLogs)) {
|
|
||||||
console.log(initLogs.join('\n'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
FlatNavigator,
|
FlatNavigator,
|
||||||
navigate,
|
navigate,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
useCreateSupportLink,
|
useCreateSupportLink,
|
||||||
} from '#/lib/hooks/useCreateSupportLink'
|
} from '#/lib/hooks/useCreateSupportLink'
|
||||||
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
|
import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
|
||||||
import {useSessionApi} from '#/state/session'
|
import {useSessionApi} from '#/state/session'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
@@ -36,8 +35,8 @@ import {
|
|||||||
isLegacyBirthdateBug,
|
isLegacyBirthdateBug,
|
||||||
useAgeAssuranceRegionConfig,
|
useAgeAssuranceRegionConfig,
|
||||||
} from '#/ageAssurance/util'
|
} from '#/ageAssurance/util'
|
||||||
import {IS_WEB} from '#/env'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {useDeviceGeolocationApi} from '#/geolocation'
|
import {useDeviceGeolocationApi} from '#/geolocation'
|
||||||
|
|
||||||
const textStyles = [a.text_md, a.leading_snug]
|
const textStyles = [a.text_md, a.leading_snug]
|
||||||
@@ -45,6 +44,7 @@ const textStyles = [a.text_md, a.leading_snug]
|
|||||||
export function NoAccessScreen() {
|
export function NoAccessScreen() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const birthdateControl = useDialogControl()
|
const birthdateControl = useDialogControl()
|
||||||
@@ -63,8 +63,8 @@ export function NoAccessScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// just counting overall hits here
|
// just counting overall hits here
|
||||||
logger.metric(`blockedGeoOverlay:shown`, {})
|
ax.metric(`blockedGeoOverlay:shown`, {})
|
||||||
logger.metric(`ageAssurance:noAccessScreen:shown`, {
|
ax.metric(`ageAssurance:noAccessScreen:shown`, {
|
||||||
accountCreatedAt: data?.accountCreatedAt || 'unknown',
|
accountCreatedAt: data?.accountCreatedAt || 'unknown',
|
||||||
isAARegion,
|
isAARegion,
|
||||||
hasDeclaredAge,
|
hasDeclaredAge,
|
||||||
@@ -103,10 +103,7 @@ export function NoAccessScreen() {
|
|||||||
label={_(msg`Click here to update your birthdate`)}
|
label={_(msg`Click here to update your birthdate`)}
|
||||||
style={[textStyles]}
|
style={[textStyles]}
|
||||||
{...createStaticClick(() => {
|
{...createStaticClick(() => {
|
||||||
logger.metric(
|
ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {})
|
||||||
'ageAssurance:noAccessScreen:openBirthdateDialog',
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
birthdateControl.open()
|
birthdateControl.open()
|
||||||
})}>
|
})}>
|
||||||
clicking here
|
clicking here
|
||||||
@@ -272,6 +269,7 @@ export function NoAccessScreen() {
|
|||||||
function AccessSection() {
|
function AccessSection() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const control = useDialogControl()
|
const control = useDialogControl()
|
||||||
const appealControl = Dialog.useDialogControl()
|
const appealControl = Dialog.useDialogControl()
|
||||||
const locationControl = Dialog.useDialogControl()
|
const locationControl = Dialog.useDialogControl()
|
||||||
@@ -305,7 +303,7 @@ function AccessSection() {
|
|||||||
label={_(msg`Contact our moderation team`)}
|
label={_(msg`Contact our moderation team`)}
|
||||||
{...createStaticClick(() => {
|
{...createStaticClick(() => {
|
||||||
appealControl.open()
|
appealControl.open()
|
||||||
logger.metric('ageAssurance:appealDialogOpen', {})
|
ax.metric('ageAssurance:appealDialogOpen', {})
|
||||||
})}>
|
})}>
|
||||||
contact our moderation team
|
contact our moderation team
|
||||||
</SimpleInlineLinkText>{' '}
|
</SimpleInlineLinkText>{' '}
|
||||||
@@ -321,7 +319,7 @@ function AccessSection() {
|
|||||||
color={hasInitiated ? 'secondary' : 'primary'}
|
color={hasInitiated ? 'secondary' : 'primary'}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
control.open()
|
control.open()
|
||||||
logger.metric('ageAssurance:initDialogOpen', {
|
ax.metric('ageAssurance:initDialogOpen', {
|
||||||
hasInitiatedPreviously: hasInitiated,
|
hasInitiatedPreviously: hasInitiated,
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icon
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {refetchAgeAssuranceServerState} from '#/ageAssurance'
|
import {refetchAgeAssuranceServerState} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_IOS, IS_WEB} from '#/env'
|
||||||
import {IS_IOS} from '#/env'
|
|
||||||
|
|
||||||
export type RedirectOverlayState = {
|
export type RedirectOverlayState = {
|
||||||
result: 'success' | 'unknown'
|
result: 'success' | 'unknown'
|
||||||
@@ -174,6 +173,7 @@ export function RedirectOverlay() {
|
|||||||
|
|
||||||
function Inner() {
|
function Inner() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const polling = useRef(false)
|
const polling = useRef(false)
|
||||||
@@ -187,7 +187,7 @@ function Inner() {
|
|||||||
|
|
||||||
polling.current = true
|
polling.current = true
|
||||||
|
|
||||||
logger.metric('ageAssurance:redirectDialogOpen', {})
|
ax.metric('ageAssurance:redirectDialogOpen', {})
|
||||||
|
|
||||||
wait(
|
wait(
|
||||||
3e3,
|
3e3,
|
||||||
@@ -218,18 +218,18 @@ function Inner() {
|
|||||||
|
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
|
|
||||||
logger.metric('ageAssurance:redirectDialogSuccess', {})
|
ax.metric('ageAssurance:redirectDialogSuccess', {})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (unmounted.current) return
|
if (unmounted.current) return
|
||||||
setError(true)
|
setError(true)
|
||||||
logger.metric('ageAssurance:redirectDialogFail', {})
|
ax.metric('ageAssurance:redirectDialogFail', {})
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unmounted.current = true
|
unmounted.current = true
|
||||||
}
|
}
|
||||||
}, [agent])
|
}, [ax, agent])
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {isNetworkError} from '#/lib/hooks/useCleanError'
|
|||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {usePatchAgeAssuranceServerState} from '#/ageAssurance'
|
import {usePatchAgeAssuranceServerState} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance/logger'
|
import {logger} from '#/ageAssurance/logger'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {BLUESKY_PROXY_DID} from '#/env'
|
import {BLUESKY_PROXY_DID} from '#/env'
|
||||||
import {useGeolocation} from '#/geolocation'
|
import {useGeolocation} from '#/geolocation'
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const IS_DEV_ENV = BLUESKY_PROXY_DID !== PUBLIC_APPVIEW_DID
|
|||||||
const APPVIEW = IS_DEV_ENV ? DEV_ENV_APPVIEW : PUBLIC_APPVIEW
|
const APPVIEW = IS_DEV_ENV ? DEV_ENV_APPVIEW : PUBLIC_APPVIEW
|
||||||
|
|
||||||
export function useBeginAgeAssurance() {
|
export function useBeginAgeAssurance() {
|
||||||
|
const ax = useAnalytics()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const geolocation = useGeolocation()
|
const geolocation = useGeolocation()
|
||||||
const patchAgeAssuranceStateResponse = usePatchAgeAssuranceServerState()
|
const patchAgeAssuranceStateResponse = usePatchAgeAssuranceServerState()
|
||||||
@@ -48,15 +50,11 @@ export function useBeginAgeAssurance() {
|
|||||||
appView.sessionManager.session.accessJwt = token
|
appView.sessionManager.session.accessJwt = token
|
||||||
appView.sessionManager.session.refreshJwt = ''
|
appView.sessionManager.session.refreshJwt = ''
|
||||||
|
|
||||||
logger.metric(
|
ax.metric('ageAssurance:api:begin', {
|
||||||
'ageAssurance:api:begin',
|
platform: Platform.OS,
|
||||||
{
|
countryCode,
|
||||||
platform: Platform.OS,
|
regionCode,
|
||||||
countryCode,
|
})
|
||||||
regionCode,
|
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 2s wait is good actually. Email sending takes a hot sec and this helps
|
* 2s wait is good actually. Email sending takes a hot sec and this helps
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {useEffect, useRef} from 'react'
|
||||||
|
|
||||||
|
import {getCurrentState, onAppStateChange} from '#/lib/appState'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks passive analytics like app foreground/background time.
|
||||||
|
*/
|
||||||
|
export function PassiveAnalytics() {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const lastActive = useRef(
|
||||||
|
getCurrentState() === 'active' ? performance.now() : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = onAppStateChange(state => {
|
||||||
|
if (state === 'active') {
|
||||||
|
lastActive.current = performance.now()
|
||||||
|
ax.metric('state:foreground', {})
|
||||||
|
} else if (lastActive.current !== null) {
|
||||||
|
ax.metric('state:background', {
|
||||||
|
secondsActive: Math.round(
|
||||||
|
(performance.now() - lastActive.current) / 1e3,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => sub.remove()
|
||||||
|
}, [ax])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {GrowthBook} from '@growthbook/growthbook-react'
|
||||||
|
|
||||||
|
import {type Metadata} from '#/analytics/metadata'
|
||||||
|
import * as env from '#/env'
|
||||||
|
|
||||||
|
export {Features} from '#/analytics/features/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We vary the amount of time we wait for GrowthBook to fetch feature
|
||||||
|
* gates based on the strategy specified.
|
||||||
|
*/
|
||||||
|
export type FeatureFetchStrategy = 'prefer-low-latency' | 'prefer-fresh-gates'
|
||||||
|
|
||||||
|
const TIMEOUT_INIT = 500 // TODO should base on p99 or something
|
||||||
|
const TIMEOUT_PREFER_LOW_LATENCY = 250
|
||||||
|
const TIMEOUT_PREFER_FRESH_GATES = 1500
|
||||||
|
|
||||||
|
export const features = new GrowthBook({
|
||||||
|
apiHost: env.GROWTHBOOK_API_HOST,
|
||||||
|
clientKey: env.GROWTHBOOK_CLIENT_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializer promise that must be awaited before using the GrowthBook
|
||||||
|
* instance or rendering the `AnalyticsFeaturesContext`. Note: this may not be
|
||||||
|
* fully initialized if it takes longer than `TIMEOUT_INIT` to initialize. In
|
||||||
|
* that case, we may see a flash of uncustomized content until the
|
||||||
|
* initialization completes.
|
||||||
|
*/
|
||||||
|
export const init = new Promise<void>(async y => {
|
||||||
|
await features.init({timeout: TIMEOUT_INIT})
|
||||||
|
y()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh feature gates from GrowthBook. Updates attributes based on the
|
||||||
|
* provided account, if any.
|
||||||
|
*/
|
||||||
|
export async function refresh({strategy}: {strategy: FeatureFetchStrategy}) {
|
||||||
|
await features.refreshFeatures({
|
||||||
|
timeout:
|
||||||
|
strategy === 'prefer-low-latency'
|
||||||
|
? TIMEOUT_PREFER_LOW_LATENCY
|
||||||
|
: TIMEOUT_PREFER_FRESH_GATES,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts our metadata into GrowthBook attributes and sets them.
|
||||||
|
*/
|
||||||
|
export function setAttributes({base, session, preferences}: Metadata) {
|
||||||
|
const {deviceId, sessionId, ...br} = base
|
||||||
|
features.setAttributes({
|
||||||
|
device_id: deviceId, // GrowthBook special field
|
||||||
|
session_id: sessionId, // GrowthBook special field
|
||||||
|
user_id: session?.did, // GrowthBook special field
|
||||||
|
id: session?.did, // GrowthBook special field
|
||||||
|
...br,
|
||||||
|
...(session || {}),
|
||||||
|
...(preferences || {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum Features {
|
||||||
|
DebugFeedContext = 'debug_show_feedcontext',
|
||||||
|
IsBskyTeam = 'is_bsky_team_member',
|
||||||
|
DisableOnboardingFindContacts = 'disable_onboarding_find_contacts',
|
||||||
|
DisableSettingsFindContacts = 'disable_settings_find_contacts',
|
||||||
|
DisableLiveNowBeta = 'disable_live_now_beta',
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import uuid from 'react-native-uuid'
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
|
||||||
|
import {device} from '#/storage'
|
||||||
|
|
||||||
|
const LEGACY_STABLE_ID = 'STATSIG_LOCAL_STORAGE_STABLE_ID'
|
||||||
|
|
||||||
|
export async function getAndMigrateDeviceId() {
|
||||||
|
const migrated = getDeviceId()
|
||||||
|
if (migrated) return migrated
|
||||||
|
const id = (await AsyncStorage.getItem(LEGACY_STABLE_ID)) || uuid.v4()
|
||||||
|
device.set(['deviceId'], id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeviceId() {
|
||||||
|
return device.get(['deviceId'])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeviceIdOrThrow() {
|
||||||
|
const id = device.get(['deviceId'])
|
||||||
|
if (!id) {
|
||||||
|
throw new Error(`deviceId is not set, call getAndMigrateDeviceId first`)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from '#/analytics/identifiers/device'
|
||||||
|
export * from '#/analytics/identifiers/session'
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
jest.mock('#/storage', () => ({
|
||||||
|
device: {
|
||||||
|
get: jest.fn(),
|
||||||
|
set: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('#/analytics/identifiers/util', () => ({
|
||||||
|
isSessionIdExpired: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('#/lib/appState', () => ({
|
||||||
|
onAppStateChange: jest.fn(() => ({remove: jest.fn()})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules()
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getMocks() {
|
||||||
|
const {device} = require('#/storage')
|
||||||
|
const {isSessionIdExpired} = require('#/analytics/identifiers/util')
|
||||||
|
return {
|
||||||
|
device: jest.mocked(device),
|
||||||
|
isSessionIdExpired: jest.mocked(isSessionIdExpired),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('session initialization', () => {
|
||||||
|
it('creates new session and sets timestamp when none exists', () => {
|
||||||
|
const {device, isSessionIdExpired} = getMocks()
|
||||||
|
device.get.mockReturnValue(undefined)
|
||||||
|
isSessionIdExpired.mockReturnValue(false)
|
||||||
|
|
||||||
|
const {getInitialSessionId} = require('./session')
|
||||||
|
const id = getInitialSessionId()
|
||||||
|
|
||||||
|
expect(id).toBeDefined()
|
||||||
|
expect(typeof id).toBe('string')
|
||||||
|
expect(device.set).toHaveBeenCalledWith(['nativeSessionId'], id)
|
||||||
|
expect(device.set).toHaveBeenCalledWith(
|
||||||
|
['nativeSessionIdLastEventAt'],
|
||||||
|
expect.any(Number),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reuses existing session when not expired', () => {
|
||||||
|
const {device, isSessionIdExpired} = getMocks()
|
||||||
|
const existingId = 'existing-session-id'
|
||||||
|
device.get.mockImplementation((key: string[]) => {
|
||||||
|
if (key[0] === 'nativeSessionId') return existingId
|
||||||
|
if (key[0] === 'nativeSessionIdLastEventAt') return Date.now()
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
isSessionIdExpired.mockReturnValue(false)
|
||||||
|
|
||||||
|
const {getInitialSessionId} = require('./session')
|
||||||
|
|
||||||
|
expect(getInitialSessionId()).toBe(existingId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates new session when existing is expired', () => {
|
||||||
|
const {device, isSessionIdExpired} = getMocks()
|
||||||
|
const existingId = 'existing-session-id'
|
||||||
|
device.get.mockImplementation((key: string[]) => {
|
||||||
|
if (key[0] === 'nativeSessionId') return existingId
|
||||||
|
if (key[0] === 'nativeSessionIdLastEventAt') return Date.now() - 999999
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
isSessionIdExpired.mockReturnValue(true)
|
||||||
|
|
||||||
|
const {getInitialSessionId} = require('./session')
|
||||||
|
const id = getInitialSessionId()
|
||||||
|
|
||||||
|
expect(id).not.toBe(existingId)
|
||||||
|
expect(device.set).toHaveBeenCalledWith(['nativeSessionId'], id)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import uuid from 'react-native-uuid'
|
||||||
|
|
||||||
|
import {onAppStateChange} from '#/lib/appState'
|
||||||
|
import {isSessionIdExpired} from '#/analytics/identifiers/util'
|
||||||
|
import {device} from '#/storage'
|
||||||
|
|
||||||
|
let sessionId = (() => {
|
||||||
|
const existing = device.get(['nativeSessionId'])
|
||||||
|
const lastEvent = device.get(['nativeSessionIdLastEventAt'])
|
||||||
|
const id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4()
|
||||||
|
device.set(['nativeSessionId'], id)
|
||||||
|
device.set(['nativeSessionIdLastEventAt'], Date.now())
|
||||||
|
return id
|
||||||
|
})()
|
||||||
|
|
||||||
|
export function getInitialSessionId() {
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSessionId() {
|
||||||
|
const [id, setId] = useState(() => sessionId)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = onAppStateChange(state => {
|
||||||
|
if (state === 'active') {
|
||||||
|
const lastEvent = device.get(['nativeSessionIdLastEventAt'])
|
||||||
|
if (isSessionIdExpired(lastEvent)) {
|
||||||
|
sessionId = uuid.v4()
|
||||||
|
device.set(['nativeSessionId'], sessionId)
|
||||||
|
setId(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device.set(['nativeSessionIdLastEventAt'], Date.now())
|
||||||
|
})
|
||||||
|
return () => sub.remove()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import uuid from 'react-native-uuid'
|
||||||
|
|
||||||
|
import {onAppStateChange} from '#/lib/appState'
|
||||||
|
import {isSessionIdExpired} from '#/analytics/identifiers/util'
|
||||||
|
|
||||||
|
const SESSION_ID_KEY = 'bsky_session_id'
|
||||||
|
const LAST_EVENT_KEY = 'bsky_session_id_last_event_at'
|
||||||
|
|
||||||
|
let sessionId = (() => {
|
||||||
|
const existing = window.sessionStorage.getItem(SESSION_ID_KEY)
|
||||||
|
const lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY)
|
||||||
|
const lastEvent = lastEventStr ? Number(lastEventStr) : undefined
|
||||||
|
const id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4()
|
||||||
|
window.sessionStorage.setItem(SESSION_ID_KEY, id)
|
||||||
|
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()))
|
||||||
|
return id
|
||||||
|
})()
|
||||||
|
|
||||||
|
export function getInitialSessionId() {
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSessionId() {
|
||||||
|
const [id, setId] = useState(() => sessionId)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = onAppStateChange(state => {
|
||||||
|
if (state === 'active') {
|
||||||
|
const lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY)
|
||||||
|
const lastEvent = lastEventStr ? Number(lastEventStr) : undefined
|
||||||
|
if (isSessionIdExpired(lastEvent)) {
|
||||||
|
sessionId = uuid.v4()
|
||||||
|
window.sessionStorage.setItem(SESSION_ID_KEY, sessionId)
|
||||||
|
setId(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()))
|
||||||
|
})
|
||||||
|
return () => sub.remove()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import * as env from '#/env'
|
||||||
|
|
||||||
|
const ONE_MIN = 60 * 1e3
|
||||||
|
const TTL = (env.IS_NATIVE ? 5 : 30) * ONE_MIN // 5 min on native
|
||||||
|
|
||||||
|
export function isSessionIdExpired(since: number | undefined) {
|
||||||
|
if (since === undefined) return false
|
||||||
|
return Date.now() - since >= TTL
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import {createContext, useContext, useEffect, useMemo} from 'react'
|
||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
import {Logger} from '#/logger'
|
||||||
|
import {
|
||||||
|
Features,
|
||||||
|
features as feats,
|
||||||
|
init,
|
||||||
|
refresh,
|
||||||
|
setAttributes,
|
||||||
|
} from '#/analytics/features'
|
||||||
|
import {
|
||||||
|
getAndMigrateDeviceId,
|
||||||
|
getDeviceId,
|
||||||
|
getInitialSessionId,
|
||||||
|
useSessionId,
|
||||||
|
} from '#/analytics/identifiers'
|
||||||
|
import {
|
||||||
|
getNavigationMetadata,
|
||||||
|
type MergeableMetadata,
|
||||||
|
type Metadata,
|
||||||
|
} from '#/analytics/metadata'
|
||||||
|
import {type Metrics, metrics} from '#/analytics/metrics'
|
||||||
|
import * as refParams from '#/analytics/misc/refParams'
|
||||||
|
import {getMetadataForLogger} from '#/analytics/utils'
|
||||||
|
import * as env from '#/env'
|
||||||
|
import {useGeolocation} from '#/geolocation'
|
||||||
|
import {device} from '#/storage'
|
||||||
|
|
||||||
|
export * as utils from '#/analytics/utils'
|
||||||
|
export const features = {init, refresh}
|
||||||
|
export {Features} from '#/analytics/features'
|
||||||
|
export {type Metrics} from '#/analytics/metrics'
|
||||||
|
|
||||||
|
type LoggerType = {
|
||||||
|
debug: Logger['debug']
|
||||||
|
info: Logger['info']
|
||||||
|
log: Logger['log']
|
||||||
|
warn: Logger['warn']
|
||||||
|
error: Logger['error']
|
||||||
|
/**
|
||||||
|
* Clones the existing logger and overrides the `context` value. Existing
|
||||||
|
* metadata is inherited.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const ax = useAnalytics()
|
||||||
|
* const logger = ax.logger.useChild(ax.logger.Context.Notifications)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
useChild: (context: Exclude<Logger['context'], undefined>) => LoggerType
|
||||||
|
Context: typeof Logger.Context
|
||||||
|
}
|
||||||
|
export type AnalyticsContextType = {
|
||||||
|
metadata: Metadata
|
||||||
|
logger: LoggerType
|
||||||
|
metric: <E extends keyof Metrics>(
|
||||||
|
event: E,
|
||||||
|
payload: Metrics[E],
|
||||||
|
metadata?: MergeableMetadata,
|
||||||
|
) => void
|
||||||
|
features: typeof Features & {
|
||||||
|
enabled(feature: Features): boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type AnalyticsBaseContextType = Omit<AnalyticsContextType, 'features'>
|
||||||
|
|
||||||
|
function createLogger(
|
||||||
|
context: Logger['context'],
|
||||||
|
metadata: Partial<Metadata>,
|
||||||
|
): LoggerType {
|
||||||
|
const logger = Logger.create(context, metadata)
|
||||||
|
return {
|
||||||
|
debug: logger.debug.bind(logger),
|
||||||
|
info: logger.info.bind(logger),
|
||||||
|
log: logger.log.bind(logger),
|
||||||
|
warn: logger.warn.bind(logger),
|
||||||
|
error: logger.error.bind(logger),
|
||||||
|
useChild: (context: Exclude<Logger['context'], undefined>) => {
|
||||||
|
return useMemo(() => createLogger(context, metadata), [context, metadata])
|
||||||
|
},
|
||||||
|
Context: Logger.Context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = createContext<AnalyticsBaseContextType>({
|
||||||
|
logger: createLogger(Logger.Context.Default, {}),
|
||||||
|
metric: (event, payload, metadata) => {
|
||||||
|
if (metadata && '__meta' in metadata) {
|
||||||
|
delete metadata.__meta
|
||||||
|
}
|
||||||
|
metrics.track(event, payload, {
|
||||||
|
...metadata,
|
||||||
|
navigation: getNavigationMetadata(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
base: {
|
||||||
|
deviceId: getDeviceId() ?? 'unknown',
|
||||||
|
sessionId: getInitialSessionId(),
|
||||||
|
platform: Platform.OS,
|
||||||
|
appVersion: env.APP_VERSION,
|
||||||
|
bundleIdentifier: env.BUNDLE_IDENTIFIER,
|
||||||
|
bundleDate: env.BUNDLE_DATE,
|
||||||
|
referrerSrc: refParams.src,
|
||||||
|
referrerUrl: refParams.url,
|
||||||
|
},
|
||||||
|
geolocation: device.get(['mergedGeolocation']) || {
|
||||||
|
countryCode: '',
|
||||||
|
regionCode: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that deviceId is set and migrated from legacy storage. Handled on
|
||||||
|
* startup in `App.<platform>.tsx`. This must be awaited prior to the app
|
||||||
|
* booting up.
|
||||||
|
*/
|
||||||
|
export const setupDeviceId = getAndMigrateDeviceId()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analytics context provider. Decorates the parent analytics context with
|
||||||
|
* additional metadata. Nesting should be done carefully and sparingly.
|
||||||
|
*/
|
||||||
|
export function AnalyticsContext({
|
||||||
|
children,
|
||||||
|
metadata,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
metadata?: MergeableMetadata
|
||||||
|
}) {
|
||||||
|
if (metadata) {
|
||||||
|
if (!('__meta' in metadata)) {
|
||||||
|
throw new Error(
|
||||||
|
'Use the useMeta() helper when passing metadata to AnalyticsContext',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sessionId = useSessionId()
|
||||||
|
const geolocation = useGeolocation()
|
||||||
|
const parentContext = useContext(Context)
|
||||||
|
const childContext = useMemo(() => {
|
||||||
|
const combinedMetadata = {
|
||||||
|
...parentContext.metadata,
|
||||||
|
...metadata,
|
||||||
|
base: {
|
||||||
|
...parentContext.metadata.base,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
geolocation,
|
||||||
|
}
|
||||||
|
const context: AnalyticsBaseContextType = {
|
||||||
|
...parentContext,
|
||||||
|
logger: createLogger(
|
||||||
|
Logger.Context.Default,
|
||||||
|
getMetadataForLogger(combinedMetadata),
|
||||||
|
),
|
||||||
|
metadata: combinedMetadata,
|
||||||
|
metric: (event, payload, extraMetadata) => {
|
||||||
|
parentContext.metric(event, payload, {
|
||||||
|
...combinedMetadata,
|
||||||
|
...extraMetadata,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}, [sessionId, geolocation, parentContext, metadata])
|
||||||
|
return <Context.Provider value={childContext}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature gates provider. Decorates the parent analytics context with
|
||||||
|
* feature gate capabilities. Should be mounted within `AnalyticsContext`,
|
||||||
|
* and below the `<Fragment key={did} />` breaker in `App.<platform>.tsx`.
|
||||||
|
*/
|
||||||
|
export function AnalyticsFeaturesContext({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const parentContext = useContext(Context)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
feats.setTrackingCallback((experiment, result) => {
|
||||||
|
parentContext.metric('experiment:viewed', {
|
||||||
|
experimentId: experiment.key,
|
||||||
|
variationId: result.key,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [parentContext.metric])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAttributes(parentContext.metadata)
|
||||||
|
}, [parentContext.metadata])
|
||||||
|
|
||||||
|
const childContext = useMemo<AnalyticsContextType>(() => {
|
||||||
|
return {
|
||||||
|
...parentContext,
|
||||||
|
features: {
|
||||||
|
enabled: feats.isOn.bind(feats),
|
||||||
|
...Features,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [parentContext])
|
||||||
|
|
||||||
|
return <Context.Provider value={childContext}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic analytics context without feature gates. Should really only be used
|
||||||
|
* above the `AnalyticsFeaturesContext` provider.
|
||||||
|
*/
|
||||||
|
export function useAnalyticsBase() {
|
||||||
|
return useContext(Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main analytics context, including feature gates. Use this everywhere you
|
||||||
|
* need metrics, features, or logging within the React tree.
|
||||||
|
*/
|
||||||
|
export function useAnalytics() {
|
||||||
|
const ctx = useContext(Context)
|
||||||
|
if (!('features' in ctx)) {
|
||||||
|
throw new Error(
|
||||||
|
'useAnalytics must be used within an AnalyticsFeaturesContext',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ctx as AnalyticsContextType
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {type Geolocation} from '#/geolocation'
|
||||||
|
|
||||||
|
export type BaseMetadata = {
|
||||||
|
deviceId: string
|
||||||
|
sessionId: string
|
||||||
|
platform: string
|
||||||
|
appVersion: string
|
||||||
|
bundleIdentifier: string
|
||||||
|
bundleDate: number
|
||||||
|
referrerSrc: string
|
||||||
|
referrerUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GeolocationMetadata = Geolocation
|
||||||
|
|
||||||
|
export type SessionMetadata = {
|
||||||
|
did: string
|
||||||
|
isBskyPds: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreferencesMetadata = {
|
||||||
|
appLanguage: string
|
||||||
|
contentLanguages: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MergeableMetadata = {
|
||||||
|
session?: SessionMetadata
|
||||||
|
preferences?: PreferencesMetadata
|
||||||
|
/**
|
||||||
|
* Navigation metadata is not actually available on this object, instead it's
|
||||||
|
* merged in at time-of-log/metric. See `#/analytics/metadata.ts` for details.
|
||||||
|
*/
|
||||||
|
navigation?: NavigationMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Metadata = {
|
||||||
|
base: BaseMetadata
|
||||||
|
geolocation: GeolocationMetadata
|
||||||
|
} & MergeableMetadata
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Navigation metadata is handle out-of-band from React, since we don't want to
|
||||||
|
* slow down screen transitions in any way, and there doesn't seem to be a nice
|
||||||
|
* way to get current navigation state without an additional re-render between
|
||||||
|
* navigations.
|
||||||
|
*
|
||||||
|
* So instead of this data being available on the Metadata object, it's stored
|
||||||
|
* here and merged in at time-of-log/metric.
|
||||||
|
*/
|
||||||
|
export type NavigationMetadata = {
|
||||||
|
previousScreen?: string
|
||||||
|
currentScreen?: string
|
||||||
|
}
|
||||||
|
let navigationMetadata: NavigationMetadata | undefined
|
||||||
|
export function getNavigationMetadata() {
|
||||||
|
console.log('metadata', JSON.stringify(navigationMetadata, null, 2))
|
||||||
|
return navigationMetadata
|
||||||
|
}
|
||||||
|
export function setNavigationMetadata(meta: NavigationMetadata | undefined) {
|
||||||
|
navigationMetadata = meta
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import {MetricsClient} from './client'
|
||||||
|
|
||||||
|
let appStateCallback: (state: string) => void
|
||||||
|
|
||||||
|
jest.mock('#/lib/appState', () => ({
|
||||||
|
onAppStateChange: jest.fn(cb => {
|
||||||
|
appStateCallback = cb
|
||||||
|
return {remove: jest.fn()}
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('#/logger', () => ({
|
||||||
|
Logger: {
|
||||||
|
create: () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}),
|
||||||
|
Context: {Metric: 'metric'},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('#/env', () => ({
|
||||||
|
METRICS_API_HOST: 'https://test.metrics.api',
|
||||||
|
IS_WEB: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
type TestEvents = {
|
||||||
|
click: {button: string}
|
||||||
|
view: {screen: string}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MetricsClient', () => {
|
||||||
|
let fetchMock: jest.Mock
|
||||||
|
let fetchRequests: {body: any}[]
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers({advanceTimers: true})
|
||||||
|
fetchRequests = []
|
||||||
|
fetchMock = jest.fn().mockImplementation(async (_url, options) => {
|
||||||
|
const body = JSON.parse(options.body)
|
||||||
|
fetchRequests.push({body})
|
||||||
|
return {ok: true, status: 200}
|
||||||
|
})
|
||||||
|
global.fetch = fetchMock
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers()
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flushes events on interval', async () => {
|
||||||
|
const client = new MetricsClient<TestEvents>()
|
||||||
|
client.track('click', {button: 'submit'})
|
||||||
|
client.track('view', {screen: 'home'})
|
||||||
|
|
||||||
|
expect(fetchRequests).toHaveLength(0)
|
||||||
|
|
||||||
|
// Advance past the 10 second interval
|
||||||
|
await jest.advanceTimersByTimeAsync(10_000)
|
||||||
|
|
||||||
|
expect(fetchRequests).toHaveLength(1)
|
||||||
|
expect(fetchRequests[0].body.events).toHaveLength(2)
|
||||||
|
expect(fetchRequests[0].body.events[0].event).toBe('click')
|
||||||
|
expect(fetchRequests[0].body.events[1].event).toBe('view')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flushes when maxBatchSize is exceeded', async () => {
|
||||||
|
const client = new MetricsClient<TestEvents>()
|
||||||
|
client.maxBatchSize = 5
|
||||||
|
|
||||||
|
// Add events up to maxBatchSize (should not flush yet)
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
client.track('click', {button: `btn-${i}`})
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(fetchRequests).toHaveLength(0)
|
||||||
|
|
||||||
|
// One more event should trigger flush (> maxBatchSize)
|
||||||
|
client.track('click', {button: 'btn-trigger'})
|
||||||
|
|
||||||
|
// Allow microtasks to run
|
||||||
|
await jest.advanceTimersByTimeAsync(0)
|
||||||
|
|
||||||
|
expect(fetchRequests).toHaveLength(1)
|
||||||
|
expect(fetchRequests[0].body.events).toHaveLength(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retries failed events once on 500 response', async () => {
|
||||||
|
let requestCount = 0
|
||||||
|
|
||||||
|
fetchMock.mockImplementation(async (_url, options) => {
|
||||||
|
requestCount++
|
||||||
|
const body = JSON.parse(options.body)
|
||||||
|
|
||||||
|
if (requestCount === 1) {
|
||||||
|
// First request fails with 500 - "Failed to fetch" triggers isNetworkError
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
text: async () => 'Internal Server Error',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry succeeds
|
||||||
|
fetchRequests.push({body})
|
||||||
|
return {ok: true, status: 200}
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = new MetricsClient<TestEvents>()
|
||||||
|
client.track('click', {button: 'submit'})
|
||||||
|
|
||||||
|
// Trigger flush via interval
|
||||||
|
await jest.advanceTimersByTimeAsync(10_000)
|
||||||
|
|
||||||
|
expect(requestCount).toBe(1)
|
||||||
|
expect(fetchRequests).toHaveLength(0)
|
||||||
|
|
||||||
|
// Simulate app coming to foreground to trigger retry
|
||||||
|
appStateCallback('active')
|
||||||
|
await jest.advanceTimersByTimeAsync(0)
|
||||||
|
|
||||||
|
expect(requestCount).toBe(2)
|
||||||
|
expect(fetchRequests).toHaveLength(1)
|
||||||
|
expect(fetchRequests[0].body.events).toHaveLength(1)
|
||||||
|
expect(fetchRequests[0].body.events[0].event).toBe('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not retry more than once', async () => {
|
||||||
|
let requestCount = 0
|
||||||
|
|
||||||
|
fetchMock.mockImplementation(async () => {
|
||||||
|
requestCount++
|
||||||
|
// Always fail with network-like error
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
text: async () => 'Internal Server Error',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = new MetricsClient<TestEvents>()
|
||||||
|
client.track('click', {button: 'submit'})
|
||||||
|
|
||||||
|
// First flush fails
|
||||||
|
await jest.advanceTimersByTimeAsync(10_000)
|
||||||
|
|
||||||
|
expect(requestCount).toBe(1)
|
||||||
|
|
||||||
|
// Retry also fails
|
||||||
|
appStateCallback('active')
|
||||||
|
await jest.advanceTimersByTimeAsync(0)
|
||||||
|
|
||||||
|
expect(requestCount).toBe(2)
|
||||||
|
|
||||||
|
// Another foreground event should not retry again (events are dropped)
|
||||||
|
appStateCallback('active')
|
||||||
|
await jest.advanceTimersByTimeAsync(0)
|
||||||
|
|
||||||
|
expect(requestCount).toBe(2) // No additional requests
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flushes when app goes to background', async () => {
|
||||||
|
const client = new MetricsClient<TestEvents>()
|
||||||
|
client.track('click', {button: 'submit'})
|
||||||
|
|
||||||
|
expect(fetchRequests).toHaveLength(0)
|
||||||
|
|
||||||
|
// Simulate app going to background
|
||||||
|
appStateCallback('background')
|
||||||
|
await jest.advanceTimersByTimeAsync(0)
|
||||||
|
|
||||||
|
expect(fetchRequests).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import {onAppStateChange} from '#/lib/appState'
|
||||||
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
|
import {Logger} from '#/logger'
|
||||||
|
import * as env from '#/env'
|
||||||
|
|
||||||
|
type Event<M extends Record<string, any>> = {
|
||||||
|
time: number
|
||||||
|
event: keyof M
|
||||||
|
payload: M[keyof M]
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRACKING_ENDPOINT = env.METRICS_API_HOST + '/t'
|
||||||
|
const logger = Logger.create(Logger.Context.Metric, {})
|
||||||
|
|
||||||
|
export class MetricsClient<M extends Record<string, any>> {
|
||||||
|
maxBatchSize = 100
|
||||||
|
|
||||||
|
private started: boolean = false
|
||||||
|
private queue: Event<M>[] = []
|
||||||
|
private failedQueue: Event<M>[] = []
|
||||||
|
private flushInterval: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.started) return
|
||||||
|
this.started = true
|
||||||
|
this.flushInterval = setInterval(() => {
|
||||||
|
this.flush()
|
||||||
|
}, 10_000)
|
||||||
|
onAppStateChange(state => {
|
||||||
|
if (state === 'active') {
|
||||||
|
this.retryFailedLogs()
|
||||||
|
} else {
|
||||||
|
this.flush()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
track<E extends keyof M>(
|
||||||
|
event: E,
|
||||||
|
payload: M[E],
|
||||||
|
metadata: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
this.start()
|
||||||
|
|
||||||
|
const e = {
|
||||||
|
time: Date.now(),
|
||||||
|
event,
|
||||||
|
payload,
|
||||||
|
metadata,
|
||||||
|
}
|
||||||
|
this.queue.push(e)
|
||||||
|
|
||||||
|
logger.info(`event: ${e.event as string}`, e)
|
||||||
|
|
||||||
|
if (this.queue.length > this.maxBatchSize) {
|
||||||
|
this.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
if (!this.queue.length) return
|
||||||
|
const events = this.queue.splice(0, this.queue.length)
|
||||||
|
this.sendBatch(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendBatch(events: Event<M>[], isRetry: boolean = false) {
|
||||||
|
logger.debug(`sendBatch: ${events.length}`, {
|
||||||
|
isRetry,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify({events})
|
||||||
|
if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) {
|
||||||
|
const success = navigator.sendBeacon(
|
||||||
|
TRACKING_ENDPOINT,
|
||||||
|
new Blob([body], {type: 'application/json'}),
|
||||||
|
)
|
||||||
|
if (!success) {
|
||||||
|
// construct a "network error" for `isNetworkError` to work
|
||||||
|
throw new Error(`Failed to fetch: sendBeacon returned false`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await fetch(TRACKING_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({events}),
|
||||||
|
keepalive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text().catch(() => 'Unknown error')
|
||||||
|
// construct a "network error" for `isNetworkError` to work
|
||||||
|
throw new Error(`${res.status} Failed to fetch — ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (isNetworkError(e)) {
|
||||||
|
if (isRetry) return // retry once
|
||||||
|
this.failedQueue.push(...events)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.error(`Failed to send metrics`, {
|
||||||
|
safeMessage: e.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private retryFailedLogs() {
|
||||||
|
if (!this.failedQueue.length) return
|
||||||
|
const events = this.failedQueue.splice(0, this.failedQueue.length)
|
||||||
|
this.sendBatch(events, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {MetricsClient} from '#/analytics/metrics/client'
|
||||||
|
import {type Events} from '#/analytics/metrics/types'
|
||||||
|
|
||||||
|
export type {Events as Metrics} from '#/analytics/metrics/types'
|
||||||
|
export * from '#/analytics/metrics/utils'
|
||||||
|
export const metrics = new MetricsClient<Events>()
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Do not import runtime code into this file
|
||||||
|
*/
|
||||||
|
|
||||||
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||||
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
||||||
|
|
||||||
export type MetricEvents = {
|
export type Events = {
|
||||||
// App events
|
// App events
|
||||||
init: {
|
init: {
|
||||||
initMs: number
|
initMs: number
|
||||||
}
|
}
|
||||||
|
'experiment:viewed': {
|
||||||
|
experimentId: string
|
||||||
|
variationId: string
|
||||||
|
}
|
||||||
|
|
||||||
'account:loggedIn': {
|
'account:loggedIn': {
|
||||||
logContext:
|
logContext:
|
||||||
| 'LoginForm'
|
| 'LoginForm'
|
||||||
@@ -139,6 +148,7 @@ export type MetricEvents = {
|
|||||||
feedUrl: string
|
feedUrl: string
|
||||||
feedType: string
|
feedType: string
|
||||||
index: number
|
index: number
|
||||||
|
reason?: string
|
||||||
}
|
}
|
||||||
'feed:endReached': {
|
'feed:endReached': {
|
||||||
feedUrl: string
|
feedUrl: string
|
||||||
@@ -374,7 +384,6 @@ export type MetricEvents = {
|
|||||||
| 'AvatarButton'
|
| 'AvatarButton'
|
||||||
| 'StarterPackProfilesList'
|
| 'StarterPackProfilesList'
|
||||||
| 'FeedInterstitial'
|
| 'FeedInterstitial'
|
||||||
| 'ProfileHeaderSuggestedFollows'
|
|
||||||
| 'PostOnboardingFindFollows'
|
| 'PostOnboardingFindFollows'
|
||||||
| 'ImmersiveVideo'
|
| 'ImmersiveVideo'
|
||||||
| 'ExploreSuggestedAccounts'
|
| 'ExploreSuggestedAccounts'
|
||||||
@@ -468,7 +477,6 @@ export type MetricEvents = {
|
|||||||
| 'AvatarButton'
|
| 'AvatarButton'
|
||||||
| 'StarterPackProfilesList'
|
| 'StarterPackProfilesList'
|
||||||
| 'FeedInterstitial'
|
| 'FeedInterstitial'
|
||||||
| 'ProfileHeaderSuggestedFollows'
|
|
||||||
| 'PostOnboardingFindFollows'
|
| 'PostOnboardingFindFollows'
|
||||||
| 'ImmersiveVideo'
|
| 'ImmersiveVideo'
|
||||||
| 'ExploreSuggestedAccounts'
|
| 'ExploreSuggestedAccounts'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function toClout(n: number | null | undefined): number | undefined {
|
||||||
|
if (n == null) {
|
||||||
|
return undefined
|
||||||
|
} else {
|
||||||
|
return Math.max(0, Math.round(Math.log(n)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* This is used for our own Bluesky post embeds, and maybe other things.
|
||||||
|
*
|
||||||
|
* In the case of our embeds, `ref_src=embed`. Not sure if `ref_url` is used.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as env from '#/env'
|
||||||
|
|
||||||
|
let refSrc = ''
|
||||||
|
let refUrl = ''
|
||||||
|
if (env.IS_WEB) {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
refSrc = params.get('ref_src') ?? ''
|
||||||
|
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const src = refSrc
|
||||||
|
export const url = refUrl
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import {useMemo} from 'react'
|
||||||
|
|
||||||
|
import {BSKY_SERVICE} from '#/lib/constants'
|
||||||
|
import {type SessionAccount} from '#/state/session'
|
||||||
|
import {
|
||||||
|
type MergeableMetadata,
|
||||||
|
type Metadata,
|
||||||
|
type SessionMetadata,
|
||||||
|
} from '#/analytics/metadata'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin `useMemo` wrapper that marks the metadata as memoized and provides a
|
||||||
|
* type guard.
|
||||||
|
*/
|
||||||
|
export function useMeta(metadata?: MergeableMetadata) {
|
||||||
|
const m = useMemo(() => metadata, [metadata])
|
||||||
|
if (!m) return
|
||||||
|
// @ts-ignore
|
||||||
|
m.__meta = true
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountToSessionMetadata(
|
||||||
|
account: SessionAccount | undefined,
|
||||||
|
): SessionMetadata | undefined {
|
||||||
|
if (!account) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
did: account.did,
|
||||||
|
isBskyPds: account.service.startsWith(BSKY_SERVICE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetadataForLogger({
|
||||||
|
base,
|
||||||
|
geolocation,
|
||||||
|
session,
|
||||||
|
}: Metadata): Record<string, any> {
|
||||||
|
return {
|
||||||
|
deviceId: base.deviceId,
|
||||||
|
sessionId: base.sessionId,
|
||||||
|
platform: base.platform,
|
||||||
|
appVersion: base.appVersion,
|
||||||
|
countryCode: geolocation.countryCode,
|
||||||
|
regionCode: geolocation.regionCode,
|
||||||
|
isBskyPds: session?.isBskyPds || 'anonymous',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {type MetricEvents} from '#/logger/metrics'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||||
@@ -38,6 +35,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
|||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {type Metrics, useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog'
|
import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog'
|
||||||
@@ -434,6 +432,7 @@ export function ProfileGrid({
|
|||||||
isVisible?: boolean
|
isVisible?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
@@ -450,12 +449,11 @@ export function ProfileGrid({
|
|||||||
const seenProfilesRef = useRef<Set<string>>(new Set())
|
const seenProfilesRef = useRef<Set<string>>(new Set())
|
||||||
const containerRef = useRef<View>(null)
|
const containerRef = useRef<View>(null)
|
||||||
const hasTrackedRef = useRef(false)
|
const hasTrackedRef = useRef(false)
|
||||||
const logContext: MetricEvents['suggestedUser:seen']['logContext'] =
|
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
|
||||||
isFeedContext
|
? 'InterstitialDiscover'
|
||||||
? 'InterstitialDiscover'
|
: isProfileHeaderContext
|
||||||
: isProfileHeaderContext
|
? 'Profile'
|
||||||
? 'Profile'
|
: 'InterstitialProfile'
|
||||||
: 'InterstitialProfile'
|
|
||||||
|
|
||||||
// Callback to fire seen events
|
// Callback to fire seen events
|
||||||
const fireSeen = useCallback(() => {
|
const fireSeen = useCallback(() => {
|
||||||
@@ -467,20 +465,16 @@ export function ProfileGrid({
|
|||||||
profilesToShow.forEach((profile, index) => {
|
profilesToShow.forEach((profile, index) => {
|
||||||
if (!seenProfilesRef.current.has(profile.did)) {
|
if (!seenProfilesRef.current.has(profile.did)) {
|
||||||
seenProfilesRef.current.add(profile.did)
|
seenProfilesRef.current.add(profile.did)
|
||||||
logger.metric(
|
ax.metric('suggestedUser:seen', {
|
||||||
'suggestedUser:seen',
|
logContext,
|
||||||
{
|
recId,
|
||||||
logContext,
|
position: index,
|
||||||
recId,
|
suggestedDid: profile.did,
|
||||||
position: index,
|
category: null,
|
||||||
suggestedDid: profile.did,
|
})
|
||||||
category: null,
|
|
||||||
},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [isLoading, error, profiles, maxLength, logContext, recId])
|
}, [ax, isLoading, error, profiles, maxLength, logContext, recId])
|
||||||
|
|
||||||
// For profile header, fire when isVisible becomes true
|
// For profile header, fire when isVisible becomes true
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -565,7 +559,7 @@ export function ProfileGrid({
|
|||||||
<ProfileCard.Link
|
<ProfileCard.Link
|
||||||
profile={profile}
|
profile={profile}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logEvent('suggestedUser:press', {
|
ax.metric('suggestedUser:press', {
|
||||||
logContext: isFeedContext
|
logContext: isFeedContext
|
||||||
? 'InterstitialDiscover'
|
? 'InterstitialDiscover'
|
||||||
: 'InterstitialProfile',
|
: 'InterstitialProfile',
|
||||||
@@ -588,7 +582,7 @@ export function ProfileGrid({
|
|||||||
onPress={e => {
|
onPress={e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onDismiss(profile.did)
|
onDismiss(profile.did)
|
||||||
logEvent('suggestedUser:dismiss', {
|
ax.metric('suggestedUser:dismiss', {
|
||||||
logContext: isFeedContext
|
logContext: isFeedContext
|
||||||
? 'InterstitialDiscover'
|
? 'InterstitialDiscover'
|
||||||
: 'InterstitialProfile',
|
: 'InterstitialProfile',
|
||||||
@@ -656,7 +650,7 @@ export function ProfileGrid({
|
|||||||
withIcon={false}
|
withIcon={false}
|
||||||
style={[a.rounded_sm]}
|
style={[a.rounded_sm]}
|
||||||
onFollow={() => {
|
onFollow={() => {
|
||||||
logEvent('suggestedUser:follow', {
|
ax.metric('suggestedUser:follow', {
|
||||||
logContext: isFeedContext
|
logContext: isFeedContext
|
||||||
? 'InterstitialDiscover'
|
? 'InterstitialDiscover'
|
||||||
: 'InterstitialProfile',
|
: 'InterstitialProfile',
|
||||||
@@ -678,7 +672,7 @@ export function ProfileGrid({
|
|||||||
// Use totalProfileCount (before dismissals) for minLength check on initial render.
|
// Use totalProfileCount (before dismissals) for minLength check on initial render.
|
||||||
const profileCountForMinCheck = totalProfileCount ?? profiles.length
|
const profileCountForMinCheck = totalProfileCount ?? profiles.length
|
||||||
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
|
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
|
||||||
logger.debug(`Not enough profiles to show suggested follows`)
|
ax.logger.debug(`Not enough profiles to show suggested follows`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,7 +706,7 @@ export function ProfileGrid({
|
|||||||
label={_(msg`See more suggested profiles`)}
|
label={_(msg`See more suggested profiles`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
followDialogControl.open()
|
followDialogControl.open()
|
||||||
logEvent('suggestedUser:seeMore', {
|
ax.metric('suggestedUser:seeMore', {
|
||||||
logContext: isFeedContext ? 'Explore' : 'Profile',
|
logContext: isFeedContext ? 'Explore' : 'Profile',
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
@@ -756,7 +750,7 @@ export function ProfileGrid({
|
|||||||
<SeeMoreSuggestedProfilesCard
|
<SeeMoreSuggestedProfilesCard
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
followDialogControl.open()
|
followDialogControl.open()
|
||||||
logger.metric('suggestedUser:seeMore', {
|
ax.metric('suggestedUser:seeMore', {
|
||||||
logContext: 'Explore',
|
logContext: 'Explore',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -794,9 +788,10 @@ function SeeMoreSuggestedProfilesCard({onPress}: {onPress: () => void}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numFeedsToDisplay = 3
|
||||||
export function SuggestedFeeds() {
|
export function SuggestedFeeds() {
|
||||||
const numFeedsToDisplay = 3
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {data, isLoading, error} = useGetPopularFeedsQuery({
|
const {data, isLoading, error} = useGetPopularFeedsQuery({
|
||||||
limit: numFeedsToDisplay,
|
limit: numFeedsToDisplay,
|
||||||
@@ -829,7 +824,7 @@ export function SuggestedFeeds() {
|
|||||||
key={feed.uri}
|
key={feed.uri}
|
||||||
view={feed}
|
view={feed}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logEvent('feed:interstitial:feedCard:press', {})
|
ax.metric('feed:interstitial:feedCard:press', {})
|
||||||
}}>
|
}}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<CardOuter
|
<CardOuter
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
import {useCleanError} from '#/lib/hooks/useCleanError'
|
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {type Shadow} from '#/state/cache/post-shadow'
|
import {type Shadow} from '#/state/cache/post-shadow'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
|
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
|
||||||
@@ -15,6 +14,7 @@ import {useTheme} from '#/alf'
|
|||||||
import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark'
|
import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
import * as toast from '#/components/Toast'
|
import * as toast from '#/components/Toast'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {PostControlButton, PostControlButtonIcon} from './PostControlButton'
|
import {PostControlButton, PostControlButtonIcon} from './PostControlButton'
|
||||||
|
|
||||||
export const BookmarkButton = memo(function BookmarkButton({
|
export const BookmarkButton = memo(function BookmarkButton({
|
||||||
@@ -29,6 +29,7 @@ export const BookmarkButton = memo(function BookmarkButton({
|
|||||||
hitSlop?: Insets
|
hitSlop?: Insets
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {mutateAsync: bookmark} = useBookmarkMutation()
|
const {mutateAsync: bookmark} = useBookmarkMutation()
|
||||||
const cleanError = useCleanError()
|
const cleanError = useCleanError()
|
||||||
@@ -52,7 +53,7 @@ export const BookmarkButton = memo(function BookmarkButton({
|
|||||||
post,
|
post,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.metric('post:bookmark', {
|
ax.metric('post:bookmark', {
|
||||||
uri: post.uri,
|
uri: post.uri,
|
||||||
authorDid: post.author.did,
|
authorDid: post.author.did,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -92,7 +93,7 @@ export const BookmarkButton = memo(function BookmarkButton({
|
|||||||
uri: post.uri,
|
uri: post.uri,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.metric('post:unbookmark', {
|
ax.metric('post:unbookmark', {
|
||||||
uri: post.uri,
|
uri: post.uri,
|
||||||
authorDid: post.author.did,
|
authorDid: post.author.did,
|
||||||
logContext,
|
logContext,
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import * as Clipboard from 'expo-clipboard'
|
|||||||
import {t} from '@lingui/macro'
|
import {t} from '@lingui/macro'
|
||||||
|
|
||||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_INTERNAL} from '#/env'
|
import {IS_INTERNAL} from '#/env'
|
||||||
|
|
||||||
export function DiscoverDebug({
|
export function DiscoverDebug({
|
||||||
@@ -15,12 +15,12 @@ export function DiscoverDebug({
|
|||||||
}: {
|
}: {
|
||||||
feedContext: string | undefined
|
feedContext: string | undefined
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const gate = useGate()
|
|
||||||
const isDiscoverDebugUser =
|
const isDiscoverDebugUser =
|
||||||
IS_INTERNAL ||
|
IS_INTERNAL ||
|
||||||
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
|
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
|
||||||
gate('debug_show_feedcontext')
|
ax.features.enabled(ax.features.DebugFeedContext)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,15 +26,17 @@ import {
|
|||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NavigationProp,
|
type NavigationProp,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {type Shadow} from '#/state/cache/post-shadow'
|
import {type Shadow} from '#/state/cache/post-shadow'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {
|
||||||
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
|
useHiddenPosts,
|
||||||
|
useHiddenPostsApi,
|
||||||
|
useLanguagePrefs,
|
||||||
|
} from '#/state/preferences'
|
||||||
import {usePinnedPostMutation} from '#/state/queries/pinned-post'
|
import {usePinnedPostMutation} from '#/state/queries/pinned-post'
|
||||||
import {
|
import {
|
||||||
usePostDeleteMutation,
|
usePostDeleteMutation,
|
||||||
@@ -71,13 +73,17 @@ import {
|
|||||||
import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
|
import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
|
||||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
import {
|
||||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
Mute_Stroke2_Corner0_Rounded as Mute,
|
||||||
|
Mute_Stroke2_Corner0_Rounded as MuteIcon,
|
||||||
|
} from '#/components/icons/Mute'
|
||||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
|
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
|
||||||
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
|
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
|
||||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
import {
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute,
|
||||||
|
SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon,
|
||||||
|
} from '#/components/icons/Speaker'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
@@ -87,6 +93,7 @@ import {
|
|||||||
useReportDialogControl,
|
useReportDialogControl,
|
||||||
} from '#/components/moderation/ReportDialog'
|
} from '#/components/moderation/ReportDialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_INTERNAL} from '#/env'
|
import {IS_INTERNAL} from '#/env'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -116,6 +123,7 @@ let PostMenuItems = ({
|
|||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
||||||
const {mutateAsync: pinPostMutate, isPending: isPinPending} =
|
const {mutateAsync: pinPostMutate, isPending: isPinPending} =
|
||||||
@@ -212,7 +220,7 @@ let PostMenuItems = ({
|
|||||||
try {
|
try {
|
||||||
if (isThreadMuted) {
|
if (isThreadMuted) {
|
||||||
unmuteThread()
|
unmuteThread()
|
||||||
logger.metric('post:unmute', {
|
ax.metric('post:unmute', {
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
authorDid: postAuthor.did,
|
authorDid: postAuthor.did,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -221,7 +229,7 @@ let PostMenuItems = ({
|
|||||||
Toast.show(_(msg`You will now receive notifications for this thread`))
|
Toast.show(_(msg`You will now receive notifications for this thread`))
|
||||||
} else {
|
} else {
|
||||||
muteThread()
|
muteThread()
|
||||||
logger.metric('post:mute', {
|
ax.metric('post:mute', {
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
authorDid: postAuthor.did,
|
authorDid: postAuthor.did,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -258,21 +266,17 @@ let PostMenuItems = ({
|
|||||||
AppBskyFeedPost.isRecord,
|
AppBskyFeedPost.isRecord,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
logger.metric(
|
ax.metric('translate', {
|
||||||
'translate',
|
sourceLanguages: post.record.langs ?? [],
|
||||||
{
|
targetLanguage: langPrefs.primaryLanguage,
|
||||||
sourceLanguages: post.record.langs ?? [],
|
textLength: post.record.text.length,
|
||||||
targetLanguage: langPrefs.primaryLanguage,
|
})
|
||||||
textLength: post.record.text.length,
|
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHidePost = () => {
|
const onHidePost = () => {
|
||||||
hidePost({uri: postUri})
|
hidePost({uri: postUri})
|
||||||
logEvent('thread:click:hideReplyForMe', {})
|
ax.metric('thread:click:hideReplyForMe', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideInPWI = !!postAuthor.labels?.find(
|
const hideInPWI = !!postAuthor.labels?.find(
|
||||||
@@ -286,7 +290,7 @@ let PostMenuItems = ({
|
|||||||
feedContext: postFeedContext,
|
feedContext: postFeedContext,
|
||||||
reqId: postReqId,
|
reqId: postReqId,
|
||||||
})
|
})
|
||||||
logger.metric('post:showMore', {
|
ax.metric('post:showMore', {
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
authorDid: postAuthor.did,
|
authorDid: postAuthor.did,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -304,7 +308,7 @@ let PostMenuItems = ({
|
|||||||
feedContext: postFeedContext,
|
feedContext: postFeedContext,
|
||||||
reqId: postReqId,
|
reqId: postReqId,
|
||||||
})
|
})
|
||||||
logger.metric('post:showLess', {
|
ax.metric('post:showLess', {
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
authorDid: postAuthor.did,
|
authorDid: postAuthor.did,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -368,7 +372,7 @@ let PostMenuItems = ({
|
|||||||
|
|
||||||
// Log metric only when hiding (not when showing)
|
// Log metric only when hiding (not when showing)
|
||||||
if (isHide) {
|
if (isHide) {
|
||||||
logEvent('thread:click:hideReplyForEveryone', {})
|
ax.metric('thread:click:hideReplyForEveryone', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.show(
|
Toast.show(
|
||||||
@@ -405,7 +409,7 @@ let PostMenuItems = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPressPin = () => {
|
const onPressPin = () => {
|
||||||
logEvent(isPinned ? 'post:unpin' : 'post:pin', {})
|
ax.metric(isPinned ? 'post:unpin' : 'post:pin', {})
|
||||||
pinPostMutate({
|
pinPostMutate({
|
||||||
postUri,
|
postUri,
|
||||||
postCid,
|
postCid,
|
||||||
@@ -458,11 +462,10 @@ let PostMenuItems = ({
|
|||||||
|
|
||||||
const onSignIn = () => requireSignIn(() => {})
|
const onSignIn = () => requireSignIn(() => {})
|
||||||
|
|
||||||
const gate = useGate()
|
|
||||||
const isDiscoverDebugUser =
|
const isDiscoverDebugUser =
|
||||||
IS_INTERNAL ||
|
IS_INTERNAL ||
|
||||||
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
|
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
|
||||||
gate('debug_show_feedcontext')
|
ax.features.enabled(ax.features.DebugFeedContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
|||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
@@ -20,9 +19,11 @@ import {useDialogContext} from '#/components/Dialog'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function RecentChats({postUri}: {postUri: string}) {
|
export function RecentChats({postUri}: {postUri: string}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const control = useDialogContext()
|
const control = useDialogContext()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {data} = useListConvosQuery({status: 'accepted'})
|
const {data} = useListConvosQuery({status: 'accepted'})
|
||||||
@@ -32,7 +33,7 @@ export function RecentChats({postUri}: {postUri: string}) {
|
|||||||
|
|
||||||
const onSelectChat = (convoId: string) => {
|
const onSelectChat = (convoId: string) => {
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
logger.metric('share:press:recentDm', {}, {statsig: true})
|
ax.metric('share:press:recentDm', {})
|
||||||
navigation.navigate('MessagesConversation', {
|
navigation.navigate('MessagesConversation', {
|
||||||
conversation: convoId,
|
conversation: convoId,
|
||||||
embed: postUri,
|
embed: postUri,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {makeProfileLink} from '#/lib/routes/links'
|
|||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {shareText, shareUrl} from '#/lib/sharing'
|
import {shareText, shareUrl} from '#/lib/sharing'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -23,6 +22,7 @@ import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/i
|
|||||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||||
import {RecentChats} from './RecentChats'
|
import {RecentChats} from './RecentChats'
|
||||||
@@ -32,6 +32,7 @@ let ShareMenuItems = ({
|
|||||||
post,
|
post,
|
||||||
onShare: onShareProp,
|
onShare: onShareProp,
|
||||||
}: ShareMenuItemsProps): React.ReactNode => {
|
}: ShareMenuItemsProps): React.ReactNode => {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
@@ -54,14 +55,14 @@ let ShareMenuItems = ({
|
|||||||
}, [postAuthor])
|
}, [postAuthor])
|
||||||
|
|
||||||
const onSharePost = () => {
|
const onSharePost = () => {
|
||||||
logger.metric('share:press:nativeShare', {}, {statsig: true})
|
ax.metric('share:press:nativeShare', {})
|
||||||
const url = toShareUrl(href)
|
const url = toShareUrl(href)
|
||||||
shareUrl(url)
|
shareUrl(url)
|
||||||
onShareProp()
|
onShareProp()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCopyLink = async () => {
|
const onCopyLink = async () => {
|
||||||
logger.metric('share:press:copyLink', {}, {statsig: true})
|
ax.metric('share:press:copyLink', {})
|
||||||
const url = toShareUrl(href)
|
const url = toShareUrl(href)
|
||||||
if (IS_IOS) {
|
if (IS_IOS) {
|
||||||
// iOS only
|
// iOS only
|
||||||
@@ -100,7 +101,7 @@ let ShareMenuItems = ({
|
|||||||
testID="postDropdownSendViaDMBtn"
|
testID="postDropdownSendViaDMBtn"
|
||||||
label={_(msg`Send via direct message`)}
|
label={_(msg`Send via direct message`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('share:press:openDmSearch', {}, {statsig: true})
|
ax.metric('share:press:openDmSearch', {})
|
||||||
sendViaChatControl.open()
|
sendViaChatControl.open()
|
||||||
}}>
|
}}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {makeProfileLink} from '#/lib/routes/links'
|
|||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {shareText, shareUrl} from '#/lib/sharing'
|
import {shareText, shareUrl} from '#/lib/sharing'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useBreakpoints} from '#/alf'
|
import {useBreakpoints} from '#/alf'
|
||||||
@@ -21,6 +20,7 @@ import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon} from '#/compon
|
|||||||
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
|
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||||
import {type ShareMenuItemsProps} from './ShareMenuItems.types'
|
import {type ShareMenuItemsProps} from './ShareMenuItems.types'
|
||||||
@@ -31,6 +31,7 @@ let ShareMenuItems = ({
|
|||||||
timestamp,
|
timestamp,
|
||||||
onShare: onShareProp,
|
onShare: onShareProp,
|
||||||
}: ShareMenuItemsProps): React.ReactNode => {
|
}: ShareMenuItemsProps): React.ReactNode => {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -56,14 +57,14 @@ let ShareMenuItems = ({
|
|||||||
}, [postAuthor])
|
}, [postAuthor])
|
||||||
|
|
||||||
const onCopyLink = () => {
|
const onCopyLink = () => {
|
||||||
logger.metric('share:press:copyLink', {}, {statsig: true})
|
ax.metric('share:press:copyLink', {})
|
||||||
const url = toShareUrl(href)
|
const url = toShareUrl(href)
|
||||||
shareUrl(url)
|
shareUrl(url)
|
||||||
onShareProp()
|
onShareProp()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelectChatToShareTo = (conversation: string) => {
|
const onSelectChatToShareTo = (conversation: string) => {
|
||||||
logger.metric('share:press:dmSelected', {}, {statsig: true})
|
ax.metric('share:press:dmSelected', {})
|
||||||
navigation.navigate('MessagesConversation', {
|
navigation.navigate('MessagesConversation', {
|
||||||
conversation,
|
conversation,
|
||||||
embed: postUri,
|
embed: postUri,
|
||||||
@@ -102,7 +103,7 @@ let ShareMenuItems = ({
|
|||||||
testID="postDropdownSendViaDMBtn"
|
testID="postDropdownSendViaDMBtn"
|
||||||
label={_(msg`Send via direct message`)}
|
label={_(msg`Send via direct message`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('share:press:openDmSearch', {}, {statsig: true})
|
ax.metric('share:press:openDmSearch', {})
|
||||||
sendViaChatControl.open()
|
sendViaChatControl.open()
|
||||||
}}>
|
}}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
@@ -117,7 +118,7 @@ let ShareMenuItems = ({
|
|||||||
testID="postDropdownEmbedBtn"
|
testID="postDropdownEmbedBtn"
|
||||||
label={_(msg`Embed post`)}
|
label={_(msg`Embed post`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('share:press:embed', {}, {statsig: true})
|
ax.metric('share:press:embed', {})
|
||||||
embedPostControl.open()
|
embedPostControl.open()
|
||||||
}}>
|
}}>
|
||||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {shareUrl} from '#/lib/sharing'
|
import {shareUrl} from '#/lib/sharing'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {type Shadow} from '#/state/cache/post-shadow'
|
import {type Shadow} from '#/state/cache/post-shadow'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
@@ -21,6 +20,7 @@ import {native} from '#/alf'
|
|||||||
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
|
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
|
||||||
import {useMenuControl} from '#/components/Menu'
|
import {useMenuControl} from '#/components/Menu'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
|
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
|
||||||
import {ShareMenuItems} from './ShareMenuItems'
|
import {ShareMenuItems} from './ShareMenuItems'
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ let ShareMenuButton = ({
|
|||||||
hitSlop?: Insets
|
hitSlop?: Insets
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {feedDescriptor} = useFeedFeedbackContext()
|
const {feedDescriptor} = useFeedFeedbackContext()
|
||||||
|
|
||||||
@@ -61,20 +62,17 @@ let ShareMenuButton = ({
|
|||||||
// menuControl.open() fires but RN doesn't expose flushSync.
|
// menuControl.open() fires but RN doesn't expose flushSync.
|
||||||
setTimeout(menuControl.open)
|
setTimeout(menuControl.open)
|
||||||
|
|
||||||
logger.metric(
|
ax.metric('post:share', {
|
||||||
'post:share',
|
uri: post.uri,
|
||||||
{
|
authorDid: post.author.did,
|
||||||
uri: post.uri,
|
logContext,
|
||||||
authorDid: post.author.did,
|
feedDescriptor,
|
||||||
logContext,
|
postContext: big ? 'thread' : 'feed',
|
||||||
feedDescriptor,
|
})
|
||||||
postContext: big ? 'thread' : 'feed',
|
|
||||||
},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
ax,
|
||||||
menuControl,
|
menuControl,
|
||||||
setHasBeenOpen,
|
setHasBeenOpen,
|
||||||
big,
|
big,
|
||||||
@@ -86,7 +84,7 @@ let ShareMenuButton = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const onNativeLongPress = () => {
|
const onNativeLongPress = () => {
|
||||||
logger.metric('share:press:nativeShare', {}, {statsig: true})
|
ax.metric('share:press:nativeShare', {})
|
||||||
const urip = new AtUri(post.uri)
|
const urip = new AtUri(post.uri)
|
||||||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
||||||
const url = toShareUrl(href)
|
const url = toShareUrl(href)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {CountWheel} from '#/lib/custom-animations/CountWheel'
|
|||||||
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +29,7 @@ import {atoms as a, useBreakpoints} from '#/alf'
|
|||||||
import {Reply as Bubble} from '#/components/icons/Reply'
|
import {Reply as Bubble} from '#/components/icons/Reply'
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||||
import * as Skele from '#/components/Skeleton'
|
import * as Skele from '#/components/Skeleton'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {BookmarkButton} from './BookmarkButton'
|
import {BookmarkButton} from './BookmarkButton'
|
||||||
import {
|
import {
|
||||||
PostControlButton,
|
PostControlButton,
|
||||||
@@ -71,6 +71,7 @@ let PostControls = ({
|
|||||||
viaRepost?: {uri: string; cid: string}
|
viaRepost?: {uri: string; cid: string}
|
||||||
variant?: 'compact' | 'normal' | 'large'
|
variant?: 'compact' | 'normal' | 'large'
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const {feedDescriptor} = useFeedFeedbackContext()
|
const {feedDescriptor} = useFeedFeedbackContext()
|
||||||
@@ -175,7 +176,7 @@ let PostControls = ({
|
|||||||
feedContext,
|
feedContext,
|
||||||
reqId,
|
reqId,
|
||||||
})
|
})
|
||||||
logger.metric('post:clickQuotePost', {
|
ax.metric('post:clickQuotePost', {
|
||||||
uri: post.uri,
|
uri: post.uri,
|
||||||
authorDid: post.author.did,
|
authorDid: post.author.did,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -226,7 +227,7 @@ let PostControls = ({
|
|||||||
!replyDisabled
|
!replyDisabled
|
||||||
? () =>
|
? () =>
|
||||||
requireAuth(() => {
|
requireAuth(() => {
|
||||||
logger.metric('post:clickReply', {
|
ax.metric('post:clickReply', {
|
||||||
uri: post.uri,
|
uri: post.uri,
|
||||||
authorDid: post.author.did,
|
authorDid: post.author.did,
|
||||||
logContext,
|
logContext,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
import {useActorStatus} from '#/lib/actor-status'
|
||||||
import {getModerationCauseKey} from '#/lib/moderation'
|
import {getModerationCauseKey} from '#/lib/moderation'
|
||||||
import {type LogEvents} from '#/lib/statsig/statsig'
|
|
||||||
import {forceLTR} from '#/lib/strings/bidi'
|
import {forceLTR} from '#/lib/strings/bidi'
|
||||||
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
@@ -47,6 +46,7 @@ import {RichText} from '#/components/RichText'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
|
import {type Metrics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function Default({
|
export function Default({
|
||||||
@@ -461,8 +461,8 @@ export function DescriptionPlaceholder({
|
|||||||
export type FollowButtonProps = {
|
export type FollowButtonProps = {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
logContext: LogEvents['profile:follow']['logContext'] &
|
logContext: Metrics['profile:follow']['logContext'] &
|
||||||
LogEvents['profile:unfollow']['logContext']
|
Metrics['profile:unfollow']['logContext']
|
||||||
colorInverted?: boolean
|
colorInverted?: boolean
|
||||||
onFollow?: () => void
|
onFollow?: () => void
|
||||||
withIcon?: boolean
|
withIcon?: boolean
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useActorSearch} from '#/state/queries/actor-search'
|
import {useActorSearch} from '#/state/queries/actor-search'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
@@ -36,6 +34,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
|||||||
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
|
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {ProgressGuideTask} from './Task'
|
import {ProgressGuideTask} from './Task'
|
||||||
@@ -67,6 +66,7 @@ export function FollowDialog({
|
|||||||
guide: Follow10ProgressGuide
|
guide: Follow10ProgressGuide
|
||||||
showArrow?: boolean
|
showArrow?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
@@ -78,7 +78,7 @@ export function FollowDialog({
|
|||||||
label={_(msg`Find people to follow`)}
|
label={_(msg`Find people to follow`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
control.open()
|
control.open()
|
||||||
logEvent('progressGuide:followDialog:open', {})
|
ax.metric('progressGuide:followDialog:open', {})
|
||||||
}}
|
}}
|
||||||
size={gtPhone ? 'small' : 'large'}
|
size={gtPhone ? 'small' : 'large'}
|
||||||
color="primary">
|
color="primary">
|
||||||
@@ -118,6 +118,7 @@ let lastSearchText = ''
|
|||||||
|
|
||||||
function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const interestsDisplayNames = useInterestsDisplayNames()
|
const interestsDisplayNames = useInterestsDisplayNames()
|
||||||
const {data: preferences} = usePreferencesQuery()
|
const {data: preferences} = usePreferencesQuery()
|
||||||
const personalizedInterests = preferences?.interests?.tags
|
const personalizedInterests = preferences?.interests?.tags
|
||||||
@@ -271,17 +272,13 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
|||||||
const position = itemsRef.current.findIndex(
|
const position = itemsRef.current.findIndex(
|
||||||
i => i.type === 'profile' && i.profile.did === item.profile.did,
|
i => i.type === 'profile' && i.profile.did === item.profile.did,
|
||||||
)
|
)
|
||||||
logger.metric(
|
ax.metric('suggestedUser:seen', {
|
||||||
'suggestedUser:seen',
|
logContext: 'ProgressGuide',
|
||||||
{
|
recId: undefined,
|
||||||
logContext: 'ProgressGuide',
|
position: position !== -1 ? position : 0,
|
||||||
recId: undefined,
|
suggestedDid: item.profile.did,
|
||||||
position: position !== -1 ? position : 0,
|
category: selectedInterestRef.current,
|
||||||
suggestedDid: item.profile.did,
|
})
|
||||||
category: selectedInterestRef.current,
|
|
||||||
},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {FloppyDisk_Stroke2_Corner0_Rounded as FloppyDiskIcon} from '#/components
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {QrCode} from '#/components/StarterPack/QrCode'
|
import {QrCode} from '#/components/StarterPack/QrCode'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export function QrCodeDialog({
|
|||||||
control: DialogControlProps
|
control: DialogControlProps
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const [isSaveProcessing, setIsSaveProcessing] = useState(false)
|
const [isSaveProcessing, setIsSaveProcessing] = useState(false)
|
||||||
const [isCopyProcessing, setIsCopyProcessing] = useState(false)
|
const [isCopyProcessing, setIsCopyProcessing] = useState(false)
|
||||||
@@ -104,7 +106,7 @@ export function QrCodeDialog({
|
|||||||
link.click()
|
link.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.metric('starterPack:share', {
|
ax.metric('starterPack:share', {
|
||||||
starterPack: starterPack.uri,
|
starterPack: starterPack.uri,
|
||||||
shareType: 'qrcode',
|
shareType: 'qrcode',
|
||||||
qrShareType: 'save',
|
qrShareType: 'save',
|
||||||
@@ -129,7 +131,7 @@ export function QrCodeDialog({
|
|||||||
navigator.clipboard.write([item])
|
navigator.clipboard.write([item])
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.metric('starterPack:share', {
|
ax.metric('starterPack:share', {
|
||||||
starterPack: starterPack.uri,
|
starterPack: starterPack.uri,
|
||||||
shareType: 'qrcode',
|
shareType: 'qrcode',
|
||||||
qrShareType: 'copy',
|
qrShareType: 'copy',
|
||||||
@@ -145,7 +147,7 @@ export function QrCodeDialog({
|
|||||||
control.close(() => {
|
control.close(() => {
|
||||||
Sharing.shareAsync(uri, {mimeType: 'image/png', UTI: 'image/png'}).then(
|
Sharing.shareAsync(uri, {mimeType: 'image/png', UTI: 'image/png'}).then(
|
||||||
() => {
|
() => {
|
||||||
logger.metric('starterPack:share', {
|
ax.metric('starterPack:share', {
|
||||||
starterPack: starterPack.uri,
|
starterPack: starterPack.uri,
|
||||||
shareType: 'qrcode',
|
shareType: 'qrcode',
|
||||||
qrShareType: 'share',
|
qrShareType: 'share',
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
|
import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
|
||||||
import {shareUrl} from '#/lib/sharing'
|
import {shareUrl} from '#/lib/sharing'
|
||||||
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {type DialogControlProps} from '#/components/Dialog'
|
import {type DialogControlProps} from '#/components/Dialog'
|
||||||
@@ -17,6 +16,7 @@ import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/ico
|
|||||||
import {QrCode_Stroke2_Corner0_Rounded as QrCodeIcon} from '#/components/icons/QrCode'
|
import {QrCode_Stroke2_Corner0_Rounded as QrCodeIcon} from '#/components/icons/QrCode'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -46,6 +46,7 @@ function ShareDialogInner({
|
|||||||
control,
|
control,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ function ShareDialogInner({
|
|||||||
const onShareLink = async () => {
|
const onShareLink = async () => {
|
||||||
if (!link) return
|
if (!link) return
|
||||||
shareUrl(link)
|
shareUrl(link)
|
||||||
logger.metric('starterPack:share', {
|
ax.metric('starterPack:share', {
|
||||||
starterPack: starterPack.uri,
|
starterPack: starterPack.uri,
|
||||||
shareType: 'link',
|
shareType: 'link',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +24,7 @@ import {Button, ButtonText} from '#/components/Button'
|
|||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Checkbox} from '#/components/forms/Toggle'
|
import {Checkbox} from '#/components/forms/Toggle'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
function WizardListCard({
|
function WizardListCard({
|
||||||
@@ -130,6 +130,7 @@ export function WizardProfileCard({
|
|||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
// Determine the "main" profile for this starter pack - either targetDid or current account
|
// Determine the "main" profile for this starter pack - either targetDid or current account
|
||||||
@@ -151,10 +152,10 @@ export function WizardProfileCard({
|
|||||||
if (profile.did === targetProfileDid) return
|
if (profile.did === targetProfileDid) return
|
||||||
|
|
||||||
if (!included) {
|
if (!included) {
|
||||||
logger.metric('starterPack:addUser', {})
|
ax.metric('starterPack:addUser', {})
|
||||||
dispatch({type: 'AddProfile', profile})
|
dispatch({type: 'AddProfile', profile})
|
||||||
} else {
|
} else {
|
||||||
logger.metric('starterPack:removeUser', {})
|
ax.metric('starterPack:removeUser', {})
|
||||||
dispatch({type: 'RemoveProfile', profileDid: profile.did})
|
dispatch({type: 'RemoveProfile', profileDid: profile.did})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {FocusGuards, FocusScope} from 'radix-ui/internal'
|
import {FocusGuards, FocusScope} from 'radix-ui/internal'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {Logo} from '#/view/icons/Logo'
|
import {Logo} from '#/view/icons/Logo'
|
||||||
import {atoms as a, flatten, useBreakpoints, web} from '#/alf'
|
import {atoms as a, flatten, useBreakpoints, web} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
const welcomeModalBg = require('../../assets/images/welcome-modal-bg.jpg')
|
const welcomeModalBg = require('../../assets/images/welcome-modal-bg.jpg')
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ interface WelcomeModalProps {
|
|||||||
|
|
||||||
export function WelcomeModal({control}: WelcomeModalProps) {
|
export function WelcomeModal({control}: WelcomeModalProps) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const [isExiting, setIsExiting] = useState(false)
|
const [isExiting, setIsExiting] = useState(false)
|
||||||
@@ -40,23 +41,24 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (control.isOpen) {
|
if (control.isOpen) {
|
||||||
logger.metric('welcomeModal:presented', {})
|
ax.metric('welcomeModal:presented', {})
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [control.isOpen])
|
}, [control.isOpen])
|
||||||
|
|
||||||
const onPressCreateAccount = () => {
|
const onPressCreateAccount = () => {
|
||||||
logger.metric('welcomeModal:signupClicked', {})
|
ax.metric('welcomeModal:signupClicked', {})
|
||||||
control.close()
|
control.close()
|
||||||
requestSwitchToAccount({requestedAccount: 'new'})
|
requestSwitchToAccount({requestedAccount: 'new'})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPressExplore = () => {
|
const onPressExplore = () => {
|
||||||
logger.metric('welcomeModal:exploreClicked', {})
|
ax.metric('welcomeModal:exploreClicked', {})
|
||||||
fadeOutAndClose()
|
fadeOutAndClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPressSignIn = () => {
|
const onPressSignIn = () => {
|
||||||
logger.metric('welcomeModal:signinClicked', {})
|
ax.metric('welcomeModal:signinClicked', {})
|
||||||
control.close()
|
control.close()
|
||||||
requestSwitchToAccount({requestedAccount: 'existing'})
|
requestSwitchToAccount({requestedAccount: 'existing'})
|
||||||
}
|
}
|
||||||
@@ -222,7 +224,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
|||||||
]}
|
]}
|
||||||
hoverStyle={[a.bg_transparent]}
|
hoverStyle={[a.bg_transparent]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('welcomeModal:dismissed', {})
|
ax.metric('welcomeModal:dismissed', {})
|
||||||
fadeOutAndClose()
|
fadeOutAndClose()
|
||||||
}}
|
}}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
|
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {
|
import {
|
||||||
type ThreadgateAllowUISetting,
|
type ThreadgateAllowUISetting,
|
||||||
threadgateViewToAllowUISetting,
|
threadgateViewToAllowUISetting,
|
||||||
@@ -36,6 +35,7 @@ import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Glo
|
|||||||
import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
|
import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -46,8 +46,9 @@ interface WhoCanReplyProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const infoDialogControl = useDialogControl()
|
const infoDialogControl = useDialogControl()
|
||||||
const editDialogControl = useDialogControl()
|
const editDialogControl = useDialogControl()
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
if (isThreadAuthor) {
|
if (isThreadAuthor) {
|
||||||
logger.metric('thread:click:editOwnThreadgate', {})
|
ax.metric('thread:click:editOwnThreadgate', {})
|
||||||
|
|
||||||
// wait on prefetch if it manages to resolve in under 200ms
|
// wait on prefetch if it manages to resolve in under 200ms
|
||||||
// otherwise, proceed immediately and show the spinner -sfn
|
// otherwise, proceed immediately and show the spinner -sfn
|
||||||
@@ -101,7 +102,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
editDialogControl.open()
|
editDialogControl.open()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger.metric('thread:click:viewSomeoneElsesThreadgate', {})
|
ax.metric('thread:click:viewSomeoneElsesThreadgate', {})
|
||||||
|
|
||||||
infoDialogControl.open()
|
infoDialogControl.open()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,11 @@ import {
|
|||||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
|
import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {platform, useTheme, web} from '#/alf'
|
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -36,6 +34,7 @@ import * as Toggle from '#/components/forms/Toggle'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -71,6 +70,7 @@ function DialogInner({
|
|||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
includeProfile?: boolean
|
includeProfile?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
@@ -133,7 +133,7 @@ function DialogInner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!activitySubscription.post && !activitySubscription.reply) {
|
if (!activitySubscription.post && !activitySubscription.reply) {
|
||||||
logger.metric('activitySubscription:disable', {})
|
ax.metric('activitySubscription:disable', {})
|
||||||
Toast.show(
|
Toast.show(
|
||||||
_(
|
_(
|
||||||
msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`,
|
msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`,
|
||||||
@@ -160,7 +160,7 @@ function DialogInner({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
logger.metric('activitySubscription:enable', {
|
ax.metric('activitySubscription:enable', {
|
||||||
setting: activitySubscription.reply ? 'posts_and_replies' : 'posts',
|
setting: activitySubscription.reply ? 'posts_and_replies' : 'posts',
|
||||||
})
|
})
|
||||||
if (!initialState.post && !initialState.reply) {
|
if (!initialState.post && !initialState.reply) {
|
||||||
@@ -177,7 +177,7 @@ function DialogInner({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: err => {
|
onError: err => {
|
||||||
logger.error('Could not save activity subscription', {message: err})
|
ax.logger.error('Could not save activity subscription', {message: err})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import {Divider} from '#/components/Divider'
|
|||||||
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {logger, useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
|
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {useDeviceGeolocationApi} from '#/geolocation'
|
import {useDeviceGeolocationApi} from '#/geolocation'
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
|
|||||||
function Inner({style}: ViewStyleProp & {}) {
|
function Inner({style}: ViewStyleProp & {}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const control = useDialogControl()
|
const control = useDialogControl()
|
||||||
const appealControl = Dialog.useDialogControl()
|
const appealControl = Dialog.useDialogControl()
|
||||||
const locationControl = Dialog.useDialogControl()
|
const locationControl = Dialog.useDialogControl()
|
||||||
@@ -138,7 +140,7 @@ function Inner({style}: ViewStyleProp & {}) {
|
|||||||
label={_(msg`Contact our moderation team`)}
|
label={_(msg`Contact our moderation team`)}
|
||||||
{...createStaticClick(() => {
|
{...createStaticClick(() => {
|
||||||
appealControl.open()
|
appealControl.open()
|
||||||
logger.metric('ageAssurance:appealDialogOpen', {})
|
ax.metric('ageAssurance:appealDialogOpen', {})
|
||||||
})}>
|
})}>
|
||||||
contact our moderation team
|
contact our moderation team
|
||||||
</InlineLinkText>{' '}
|
</InlineLinkText>{' '}
|
||||||
@@ -167,7 +169,7 @@ function Inner({style}: ViewStyleProp & {}) {
|
|||||||
color={hasInitiated ? 'secondary' : 'primary'}
|
color={hasInitiated ? 'secondary' : 'primary'}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
control.open()
|
control.open()
|
||||||
logger.metric('ageAssurance:initDialogOpen', {
|
ax.metric('ageAssurance:initDialogOpen', {
|
||||||
hasInitiatedPreviously: hasInitiated,
|
hasInitiatedPreviously: hasInitiated,
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/
|
|||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function AgeAssuranceAdmonition({
|
export function AgeAssuranceAdmonition({
|
||||||
children,
|
children,
|
||||||
@@ -40,6 +40,7 @@ function Inner({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -92,7 +93,7 @@ function Inner({
|
|||||||
to={'/settings/account'}
|
to={'/settings/account'}
|
||||||
style={[a.text_sm, a.leading_snug, a.font_semi_bold]}
|
style={[a.text_sm, a.leading_snug, a.font_semi_bold]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('ageAssurance:navigateToSettings', {})
|
ax.metric('ageAssurance:navigateToSettings', {})
|
||||||
}}>
|
}}>
|
||||||
account settings.
|
account settings.
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import * as Dialog from '#/components/Dialog'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {logger} from '#/ageAssurance'
|
import {logger} from '#/ageAssurance'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function AgeAssuranceAppealDialog({
|
export function AgeAssuranceAppealDialog({
|
||||||
control,
|
control,
|
||||||
@@ -37,6 +38,7 @@ export function AgeAssuranceAppealDialog({
|
|||||||
|
|
||||||
function Inner({control}: {control: Dialog.DialogControlProps}) {
|
function Inner({control}: {control: Dialog.DialogControlProps}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
@@ -46,7 +48,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
|
|||||||
|
|
||||||
const {mutate, isPending} = useMutation({
|
const {mutate, isPending} = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
logger.metric('ageAssurance:appealDialogSubmit', {})
|
ax.metric('ageAssurance:appealDialogSubmit', {})
|
||||||
|
|
||||||
await agent.createModerationReport(
|
await agent.createModerationReport(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
|||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function useInternalState() {
|
export function useInternalState() {
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
@@ -42,6 +42,7 @@ export function useInternalState() {
|
|||||||
|
|
||||||
export function AgeAssuranceDismissibleFeedBanner() {
|
export function AgeAssuranceDismissibleFeedBanner() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {visible, close} = useInternalState()
|
const {visible, close} = useInternalState()
|
||||||
const copy = useAgeAssuranceCopy()
|
const copy = useAgeAssuranceCopy()
|
||||||
@@ -66,7 +67,7 @@ export function AgeAssuranceDismissibleFeedBanner() {
|
|||||||
to="/settings/account"
|
to="/settings/account"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
close()
|
close()
|
||||||
logger.metric('ageAssurance:navigateToSettings', {})
|
ax.metric('ageAssurance:navigateToSettings', {})
|
||||||
}}
|
}}
|
||||||
style={[a.w_full, a.justify_between, a.align_center, a.gap_md]}>
|
style={[a.w_full, a.justify_between, a.align_center, a.gap_md]}>
|
||||||
<View
|
<View
|
||||||
@@ -105,7 +106,7 @@ export function AgeAssuranceDismissibleFeedBanner() {
|
|||||||
size="small"
|
size="small"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
close()
|
close()
|
||||||
logger.metric('ageAssurance:dismissFeedBanner', {})
|
ax.metric('ageAssurance:dismissFeedBanner', {})
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy
|
|||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
|
export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
const {nux} = useNux(Nux.AgeAssuranceDismissibleNotice)
|
const {nux} = useNux(Nux.AgeAssuranceDismissibleNotice)
|
||||||
const copy = useAgeAssuranceCopy()
|
const copy = useAgeAssuranceCopy()
|
||||||
@@ -45,7 +46,7 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
|
|||||||
completed: true,
|
completed: true,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
})
|
})
|
||||||
logger.metric('ageAssurance:dismissSettingsNotice', {})
|
ax.metric('ageAssurance:dismissSettingsNotice', {})
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import {useSession} from '#/state/session'
|
|||||||
import {atoms as a, web} from '#/alf'
|
import {atoms as a, web} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||||
import {urls} from '#/components/ageAssurance/const'
|
import {KWS_SUPPORTED_LANGS, urls} from '#/components/ageAssurance/const'
|
||||||
import {KWS_SUPPORTED_LANGS} from '#/components/ageAssurance/const'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
@@ -30,9 +29,9 @@ import {LanguageSelect} from '#/components/LanguageSelect'
|
|||||||
import {SimpleInlineLinkText} from '#/components/Link'
|
import {SimpleInlineLinkText} from '#/components/Link'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {logger} from '#/ageAssurance'
|
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance'
|
import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export {useDialogControl} from '#/components/Dialog/context'
|
export {useDialogControl} from '#/components/Dialog/context'
|
||||||
|
|
||||||
@@ -64,6 +63,7 @@ export function AgeAssuranceInitDialog({
|
|||||||
|
|
||||||
function Inner() {
|
function Inner() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const cleanError = useCleanError()
|
const cleanError = useCleanError()
|
||||||
@@ -116,7 +116,7 @@ function Inner() {
|
|||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
setLanguageError(false)
|
setLanguageError(false)
|
||||||
|
|
||||||
logger.metric('ageAssurance:initDialogSubmit', {})
|
ax.metric('ageAssurance:initDialogSubmit', {})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {status} = runEmailValidation()
|
const {status} = runEmailValidation()
|
||||||
@@ -143,7 +143,7 @@ function Inner() {
|
|||||||
error = _(
|
error = _(
|
||||||
msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`,
|
msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`,
|
||||||
)
|
)
|
||||||
logger.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'})
|
ax.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'})
|
||||||
} else if (e.error === 'DidTooLong') {
|
} else if (e.error === 'DidTooLong') {
|
||||||
error = (
|
error = (
|
||||||
<>
|
<>
|
||||||
@@ -159,14 +159,14 @@ function Inner() {
|
|||||||
</Trans>
|
</Trans>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
logger.metric('ageAssurance:initDialogError', {code: 'DidTooLong'})
|
ax.metric('ageAssurance:initDialogError', {code: 'DidTooLong'})
|
||||||
} else {
|
} else {
|
||||||
logger.metric('ageAssurance:initDialogError', {code: 'other'})
|
ax.metric('ageAssurance:initDialogError', {code: 'other'})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const {clean, raw} = cleanError(e)
|
const {clean, raw} = cleanError(e)
|
||||||
error = clean || raw || error
|
error = clean || raw || error
|
||||||
logger.metric('ageAssurance:initDialogError', {code: 'other'})
|
ax.metric('ageAssurance:initDialogError', {code: 'other'})
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(error)
|
setError(error)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icon
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {refetchAgeAssuranceServerState} from '#/ageAssurance'
|
import {refetchAgeAssuranceServerState} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
|
|
||||||
export type AgeAssuranceRedirectDialogState = {
|
export type AgeAssuranceRedirectDialogState = {
|
||||||
@@ -81,6 +81,7 @@ export function AgeAssuranceRedirectDialog() {
|
|||||||
|
|
||||||
export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const polling = useRef(false)
|
const polling = useRef(false)
|
||||||
@@ -94,7 +95,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
|||||||
|
|
||||||
polling.current = true
|
polling.current = true
|
||||||
|
|
||||||
logger.metric('ageAssurance:redirectDialogOpen', {})
|
ax.metric('ageAssurance:redirectDialogOpen', {})
|
||||||
|
|
||||||
wait(
|
wait(
|
||||||
3e3,
|
3e3,
|
||||||
@@ -125,18 +126,18 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
|||||||
|
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
|
|
||||||
logger.metric('ageAssurance:redirectDialogSuccess', {})
|
ax.metric('ageAssurance:redirectDialogSuccess', {})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (unmounted.current) return
|
if (unmounted.current) return
|
||||||
setError(true)
|
setError(true)
|
||||||
logger.metric('ageAssurance:redirectDialogFail', {})
|
ax.metric('ageAssurance:redirectDialogFail', {})
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unmounted.current = true
|
unmounted.current = true
|
||||||
}
|
}
|
||||||
}, [agent, control])
|
}, [ax, agent, control])
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import * as Layout from '#/components/Layout'
|
|||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {logger} from '#/ageAssurance'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function AgeRestrictedScreen({
|
export function AgeRestrictedScreen({
|
||||||
children,
|
children,
|
||||||
@@ -27,6 +27,7 @@ export function AgeRestrictedScreen({
|
|||||||
rightHeaderSlot?: React.ReactNode
|
rightHeaderSlot?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const copy = useAgeAssuranceCopy()
|
const copy = useAgeAssuranceCopy()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ export function AgeRestrictedScreen({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('ageAssurance:navigateToSettings', {})
|
ax.metric('ageAssurance:navigateToSettings', {})
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Go to account settings</Trans>
|
<Trans>Go to account settings</Trans>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import {Link} from '../Link'
|
import {Link} from '../Link'
|
||||||
import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist'
|
import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist'
|
||||||
@@ -19,6 +19,7 @@ import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allow
|
|||||||
export function FindContactsBannerNUX() {
|
export function FindContactsBannerNUX() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {visible, close} = useInternalState()
|
const {visible, close} = useInternalState()
|
||||||
|
|
||||||
if (!visible) return null
|
if (!visible) return null
|
||||||
@@ -30,7 +31,7 @@ export function FindContactsBannerNUX() {
|
|||||||
to={{screen: 'FindContactsFlow'}}
|
to={{screen: 'FindContactsFlow'}}
|
||||||
label={_(msg`Import contacts to find your friends`)}
|
label={_(msg`Import contacts to find your friends`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('contacts:nux:bannerPressed', {})
|
ax.metric('contacts:nux:bannerPressed', {})
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
@@ -84,6 +85,7 @@ export function FindContactsBannerNUX() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
function useInternalState() {
|
function useInternalState() {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {nux} = useNux(Nux.FindContactsDismissibleBanner)
|
const {nux} = useNux(Nux.FindContactsDismissibleBanner)
|
||||||
const {mutate: save, variables} = useSaveNux()
|
const {mutate: save, variables} = useSaveNux()
|
||||||
const hidden = !!variables
|
const hidden = !!variables
|
||||||
@@ -103,7 +105,7 @@ function useInternalState() {
|
|||||||
completed: true,
|
completed: true,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
})
|
})
|
||||||
logger.metric('contacts:nux:bannerDismissed', {})
|
ax.metric('contacts:nux:bannerDismissed', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {visible, close}
|
return {visible, close}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import * as Layout from '#/components/Layout'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {
|
import {
|
||||||
contactsWithPhoneNumbersOnly,
|
contactsWithPhoneNumbersOnly,
|
||||||
filterMatchedNumbers,
|
filterMatchedNumbers,
|
||||||
@@ -51,6 +52,7 @@ export function GetContacts({
|
|||||||
context: 'Onboarding' | 'Standalone'
|
context: 'Onboarding' | 'Standalone'
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const gutters = useGutters([0, 'wide'])
|
const gutters = useGutters([0, 'wide'])
|
||||||
@@ -100,16 +102,16 @@ export function GetContacts({
|
|||||||
},
|
},
|
||||||
onSuccess: (result, contacts) => {
|
onSuccess: (result, contacts) => {
|
||||||
if (context === 'Onboarding') {
|
if (context === 'Onboarding') {
|
||||||
logger.metric('onboarding:contacts:contactsShared', {})
|
ax.metric('onboarding:contacts:contactsShared', {})
|
||||||
}
|
}
|
||||||
if (result.matches.length > 0) {
|
if (result.matches.length > 0) {
|
||||||
logger.metric('contacts:import:success', {
|
ax.metric('contacts:import:success', {
|
||||||
contactCount: contacts.length,
|
contactCount: contacts.length,
|
||||||
matchCount: result.matches.length,
|
matchCount: result.matches.length,
|
||||||
entryPoint: context,
|
entryPoint: context,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger.metric('contacts:import:failure', {
|
ax.metric('contacts:import:failure', {
|
||||||
reason: 'noValidNumbers',
|
reason: 'noValidNumbers',
|
||||||
entryPoint: context,
|
entryPoint: context,
|
||||||
})
|
})
|
||||||
@@ -134,7 +136,7 @@ export function GetContacts({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: err => {
|
onError: err => {
|
||||||
logger.metric('contacts:import:failure', {
|
ax.metric('contacts:import:failure', {
|
||||||
reason: isNetworkError(err) ? 'networkError' : 'unknown',
|
reason: isNetworkError(err) ? 'networkError' : 'unknown',
|
||||||
entryPoint: context,
|
entryPoint: context,
|
||||||
})
|
})
|
||||||
@@ -180,7 +182,7 @@ export function GetContacts({
|
|||||||
permissions = await Contacts.requestPermissionsAsync()
|
permissions = await Contacts.requestPermissionsAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.metric('contacts:permission:request', {
|
ax.metric('contacts:permission:request', {
|
||||||
status: permissions.granted ? 'granted' : 'denied',
|
status: permissions.granted ? 'granted' : 'denied',
|
||||||
accessLevelIOS: ios(permissions.accessPrivileges),
|
accessLevelIOS: ios(permissions.accessPrivileges),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import * as Layout from '#/components/Layout'
|
|||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {useGeolocation} from '#/geolocation'
|
import {useGeolocation} from '#/geolocation'
|
||||||
import {isFindContactsFeatureEnabled} from '../country-allowlist'
|
import {isFindContactsFeatureEnabled} from '../country-allowlist'
|
||||||
import {
|
import {
|
||||||
@@ -52,6 +53,7 @@ export function PhoneInput({
|
|||||||
onSkip: () => void
|
onSkip: () => void
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const location = useGeolocation()
|
const location = useGeolocation()
|
||||||
@@ -85,7 +87,7 @@ export function PhoneInput({
|
|||||||
payload: {phoneCountryCode, phoneNumber},
|
payload: {phoneCountryCode, phoneNumber},
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.metric('contacts:phone:phoneEntered', {entryPoint: context})
|
ax.metric('contacts:phone:phoneEntered', {entryPoint: context})
|
||||||
},
|
},
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import * as Layout from '#/components/Layout'
|
|||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {OTPInput} from '../components/OTPInput'
|
import {OTPInput} from '../components/OTPInput'
|
||||||
import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number'
|
import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number'
|
||||||
import {type Action, type State, useOnPressBackButton} from '../state'
|
import {type Action, type State, useOnPressBackButton} from '../state'
|
||||||
@@ -40,6 +41,7 @@ export function VerifyNumber({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const gutters = useGutters([0, 'wide'])
|
const gutters = useGutters([0, 'wide'])
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ export function VerifyNumber({
|
|||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
logger.metric('contacts:phone:phoneVerified', {entryPoint: context})
|
ax.metric('contacts:phone:phoneVerified', {entryPoint: context})
|
||||||
},
|
},
|
||||||
onMutate: () => setError(null),
|
onMutate: () => setError(null),
|
||||||
onError: err => {
|
onError: err => {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {Loader} from '#/components/Loader'
|
|||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {InviteInfo} from '../components/InviteInfo'
|
import {InviteInfo} from '../components/InviteInfo'
|
||||||
import {type Action, type Contact, type Match, type State} from '../state'
|
import {type Action, type Contact, type Match, type State} from '../state'
|
||||||
@@ -83,6 +84,7 @@ export function ViewMatches({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const gutter = useGutters([0, 'wide'])
|
const gutter = useGutters([0, 'wide'])
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -109,9 +111,9 @@ export function ViewMatches({
|
|||||||
|
|
||||||
const cumulativeFollowCount = useRef(0)
|
const cumulativeFollowCount = useRef(0)
|
||||||
const onFollow = useCallback(() => {
|
const onFollow = useCallback(() => {
|
||||||
logger.metric('contacts:matches:follow', {entryPoint: context})
|
ax.metric('contacts:matches:follow', {entryPoint: context})
|
||||||
cumulativeFollowCount.current += 1
|
cumulativeFollowCount.current += 1
|
||||||
}, [context])
|
}, [ax, context])
|
||||||
|
|
||||||
const {mutate: followAll, isPending: isFollowingAll} = useMutation({
|
const {mutate: followAll, isPending: isFollowingAll} = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -132,7 +134,7 @@ export function ViewMatches({
|
|||||||
return followableDids
|
return followableDids
|
||||||
},
|
},
|
||||||
onMutate: () =>
|
onMutate: () =>
|
||||||
logger.metric('contacts:matches:followAll', {
|
ax.metric('contacts:matches:followAll', {
|
||||||
followCount: followableDids.length,
|
followCount: followableDids.length,
|
||||||
entryPoint: context,
|
entryPoint: context,
|
||||||
}),
|
}),
|
||||||
@@ -218,7 +220,7 @@ export function ViewMatches({
|
|||||||
await agent.app.bsky.contact.dismissMatch({subject: did})
|
await agent.app.bsky.contact.dismissMatch({subject: did})
|
||||||
},
|
},
|
||||||
onMutate: did => {
|
onMutate: did => {
|
||||||
logger.metric('contacts:matches:dismiss', {entryPoint: context})
|
ax.metric('contacts:matches:dismiss', {entryPoint: context})
|
||||||
dispatch({type: 'DISMISS_MATCH', payload: {did}})
|
dispatch({type: 'DISMISS_MATCH', payload: {did}})
|
||||||
},
|
},
|
||||||
onSuccess: (_res, did) => {
|
onSuccess: (_res, did) => {
|
||||||
@@ -392,7 +394,7 @@ export function ViewMatches({
|
|||||||
label={context === 'Onboarding' ? _(msg`Next`) : _(msg`Done`)}
|
label={context === 'Onboarding' ? _(msg`Next`) : _(msg`Done`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (context === 'Onboarding') {
|
if (context === 'Onboarding') {
|
||||||
logger.metric('onboarding:contacts:nextPressed', {
|
ax.metric('onboarding:contacts:nextPressed', {
|
||||||
matchCount: allMatches.length,
|
matchCount: allMatches.length,
|
||||||
followCount: cumulativeFollowCount.current,
|
followCount: cumulativeFollowCount.current,
|
||||||
dismissedMatchCount: state.dismissedMatches.length,
|
dismissedMatchCount: state.dismissedMatches.length,
|
||||||
@@ -516,6 +518,7 @@ function ContactItem({
|
|||||||
const gutter = useGutters([0, 'wide'])
|
const gutter = useGutters([0, 'wide'])
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const name = contact.name ?? contact.firstName ?? contact.lastName
|
const name = contact.name ?? contact.firstName ?? contact.lastName
|
||||||
@@ -564,7 +567,7 @@ function ContactItem({
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
logger.metric('contacts:matches:invite', {
|
ax.metric('contacts:matches:invite', {
|
||||||
entryPoint: context,
|
entryPoint: context,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {Image} from 'expo-image'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {
|
import {
|
||||||
type Gif,
|
type Gif,
|
||||||
@@ -30,6 +29,7 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
|||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
||||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
|
||||||
export function GifSelectDialog({
|
export function GifSelectDialog({
|
||||||
@@ -280,14 +280,15 @@ export function GifPreview({
|
|||||||
gif: Gif
|
gif: Gif
|
||||||
onSelectGif: (gif: Gif) => void
|
onSelectGif: (gif: Gif) => void
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {gtTablet} = useBreakpoints()
|
const {gtTablet} = useBreakpoints()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
logEvent('composer:gif:select', {})
|
ax.metric('composer:gif:select', {})
|
||||||
onSelectGif(gif)
|
onSelectGif(gif)
|
||||||
}, [onSelectGif, gif])
|
}, [ax, onSelectGif, gif])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {useMyListsQuery} from '#/state/queries/my-lists'
|
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||||
import {useGetPost} from '#/state/queries/post'
|
import {useGetPost} from '#/state/queries/post'
|
||||||
@@ -51,6 +50,7 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/ico
|
|||||||
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
|
|
||||||
export type PostInteractionSettingsFormProps = {
|
export type PostInteractionSettingsFormProps = {
|
||||||
@@ -80,8 +80,9 @@ export function PostInteractionSettingsControlledDialog({
|
|||||||
}: PostInteractionSettingsFormProps & {
|
}: PostInteractionSettingsFormProps & {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const onClose = useNonReactiveCallback(() => {
|
const onClose = useNonReactiveCallback(() => {
|
||||||
logger.metric('composer:threadgate:save', {
|
ax.metric('composer:threadgate:save', {
|
||||||
hasChanged: !!rest.isDirty,
|
hasChanged: !!rest.isDirty,
|
||||||
persist: !!rest.persist,
|
persist: !!rest.persist,
|
||||||
replyOptions:
|
replyOptions:
|
||||||
@@ -161,6 +162,7 @@ export function PostInteractionSettingsDialog(
|
|||||||
export function PostInteractionSettingsDialogControlledInner(
|
export function PostInteractionSettingsDialogControlledInner(
|
||||||
props: PostInteractionSettingsDialogProps,
|
props: PostInteractionSettingsDialogProps,
|
||||||
) {
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
@@ -229,7 +231,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
|
|
||||||
props.control.close()
|
props.control.close()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Failed to save post interaction settings`, {
|
ax.logger.error(`Failed to save post interaction settings`, {
|
||||||
source: 'PostInteractionSettingsDialogControlledInner',
|
source: 'PostInteractionSettingsDialogControlledInner',
|
||||||
safeMessage: e.message,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
@@ -244,6 +246,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
_,
|
_,
|
||||||
|
ax,
|
||||||
props.postUri,
|
props.postUri,
|
||||||
props.rootPostUri,
|
props.rootPostUri,
|
||||||
props.control,
|
props.control,
|
||||||
@@ -689,6 +692,7 @@ export function usePrefetchPostInteractionSettings({
|
|||||||
postUri: string
|
postUri: string
|
||||||
rootPostUri: string
|
rootPostUri: string
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const getPost = useGetPost()
|
const getPost = useGetPost()
|
||||||
@@ -712,9 +716,9 @@ export function usePrefetchPostInteractionSettings({
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Failed to prefetch post interaction settings`, {
|
ax.logger.error(`Failed to prefetch post interaction settings`, {
|
||||||
safeMessage: e.message,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [queryClient, agent, postUri, rootPostUri, getPost])
|
}, [ax, queryClient, agent, postUri, rootPostUri, getPost])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import {useCallback, useImperativeHandle, useRef, useState} from 'react'
|
import {useCallback, useImperativeHandle, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {useWindowDimensions, View} from 'react-native'
|
||||||
import {useWindowDimensions} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {BSKY_SERVICE} from '#/lib/constants'
|
import {BSKY_SERVICE} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
@@ -17,6 +15,7 @@ import * as TextField from '#/components/forms/TextField'
|
|||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom'
|
type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom'
|
||||||
|
|
||||||
@@ -27,6 +26,7 @@ export function ServerInputDialog({
|
|||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
onSelect: (url: string) => void
|
onSelect: (url: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {height} = useWindowDimensions()
|
const {height} = useWindowDimensions()
|
||||||
const formRef = useRef<DialogInnerRef>(null)
|
const formRef = useRef<DialogInnerRef>(null)
|
||||||
|
|
||||||
@@ -43,10 +43,10 @@ export function ServerInputDialog({
|
|||||||
setPreviousCustomAddress(result)
|
setPreviousCustomAddress(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.metric('signin:hostingProviderPressed', {
|
ax.metric('signin:hostingProviderPressed', {
|
||||||
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
|
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
|
||||||
})
|
})
|
||||||
}, [onSelect, fixedOption])
|
}, [ax, onSelect, fixedOption])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {
|
import {
|
||||||
invalidateActorStarterPacksWithMembershipQuery,
|
invalidateActorStarterPacksWithMembershipQuery,
|
||||||
useActorStarterPacksWithMembershipsQuery,
|
useActorStarterPacksWithMembershipsQuery,
|
||||||
@@ -31,6 +30,7 @@ import {StarterPack} from '#/components/icons/StarterPack'
|
|||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -244,8 +244,9 @@ function StarterPackItem({
|
|||||||
starterPackWithMembership: StarterPackWithMembership
|
starterPackWithMembership: StarterPackWithMembership
|
||||||
targetDid: string
|
targetDid: string
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {_} = useLingui()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const starterPack = starterPackWithMembership.starterPack
|
const starterPack = starterPackWithMembership.starterPack
|
||||||
@@ -304,7 +305,7 @@ function StarterPackItem({
|
|||||||
listUri: listUri,
|
listUri: listUri,
|
||||||
actorDid: targetDid,
|
actorDid: targetDid,
|
||||||
})
|
})
|
||||||
logger.metric('starterPack:addUser', {starterPack: starterPackUri})
|
ax.metric('starterPack:addUser', {starterPack: starterPackUri})
|
||||||
} else {
|
} else {
|
||||||
if (!starterPackWithMembership.listItem?.uri) {
|
if (!starterPackWithMembership.listItem?.uri) {
|
||||||
console.error('Cannot remove: missing membership URI')
|
console.error('Cannot remove: missing membership URI')
|
||||||
@@ -316,7 +317,7 @@ function StarterPackItem({
|
|||||||
actorDid: targetDid,
|
actorDid: targetDid,
|
||||||
membershipUri: starterPackWithMembership.listItem.uri,
|
membershipUri: starterPackWithMembership.listItem.uri,
|
||||||
})
|
})
|
||||||
logger.metric('starterPack:removeUser', {starterPack: starterPackUri})
|
ax.metric('starterPack:removeUser', {starterPack: starterPackUri})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {LinearGradient} from 'expo-linear-gradient'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist'
|
import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist'
|
||||||
@@ -16,8 +15,8 @@ import {
|
|||||||
isExistingUserAsOf,
|
isExistingUserAsOf,
|
||||||
} from '#/components/dialogs/nuxs/utils'
|
} from '#/components/dialogs/nuxs/utils'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_E2E} from '#/env'
|
import {IS_E2E, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {navigate} from '#/Navigation'
|
import {navigate} from '#/Navigation'
|
||||||
|
|
||||||
export const enabled = createIsEnabledCheck(props => {
|
export const enabled = createIsEnabledCheck(props => {
|
||||||
@@ -35,6 +34,7 @@ export const enabled = createIsEnabledCheck(props => {
|
|||||||
export function FindContactsAnnouncement() {
|
export function FindContactsAnnouncement() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const nuxDialogs = useNuxDialogContext()
|
const nuxDialogs = useNuxDialogContext()
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export function FindContactsAnnouncement() {
|
|||||||
size="large"
|
size="large"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('contacts:nux:ctaPressed', {})
|
ax.metric('contacts:nux:ctaPressed', {})
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
navigate('FindContactsFlow')
|
navigate('FindContactsFlow')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {urls} from '#/lib/constants'
|
import {urls} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
@@ -14,11 +13,13 @@ import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons
|
|||||||
import {VerifierCheck} from '#/components/icons/VerifierCheck'
|
import {VerifierCheck} from '#/components/icons/VerifierCheck'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Span, Text} from '#/components/Typography'
|
import {Span, Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
|
|
||||||
export function InitialVerificationAnnouncement() {
|
export function InitialVerificationAnnouncement() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const nuxDialogs = useNuxDialogContext()
|
const nuxDialogs = useNuxDialogContext()
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
@@ -161,13 +162,9 @@ export function InitialVerificationAnnouncement() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
style={[a.justify_center, a.w_full]}
|
style={[a.justify_center, a.w_full]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric(
|
ax.metric('verification:learn-more', {
|
||||||
'verification:learn-more',
|
location: 'initialAnnouncementeNux',
|
||||||
{
|
})
|
||||||
location: 'initialAnnouncementeNux',
|
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Read blog post</Trans>
|
<Trans>Read blog post</Trans>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const enabled = createIsEnabledCheck(props => {
|
|||||||
'2026-01-16T00:00:00.000Z',
|
'2026-01-16T00:00:00.000Z',
|
||||||
props.currentProfile.createdAt,
|
props.currentProfile.createdAt,
|
||||||
) &&
|
) &&
|
||||||
!props.gate('disable_live_now_beta')
|
!props.features.enabled(props.features.DisableLiveNowBeta)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {type AppBskyActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
|
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
|
||||||
@@ -25,6 +24,7 @@ import {
|
|||||||
} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
|
} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
|
||||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||||
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
|
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {useGeolocation} from '#/geolocation'
|
import {useGeolocation} from '#/geolocation'
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
@@ -88,7 +88,7 @@ function Inner({
|
|||||||
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
preferences: UsePreferencesQueryResponse
|
preferences: UsePreferencesQueryResponse
|
||||||
}) {
|
}) {
|
||||||
const gate = useGate()
|
const ax = useAnalytics()
|
||||||
const geolocation = useGeolocation()
|
const geolocation = useGeolocation()
|
||||||
const {nuxs} = useNuxs()
|
const {nuxs} = useNuxs()
|
||||||
const [snoozed, setSnoozed] = useState(() => {
|
const [snoozed, setSnoozed] = useState(() => {
|
||||||
@@ -133,7 +133,7 @@ function Inner({
|
|||||||
if (
|
if (
|
||||||
enabled &&
|
enabled &&
|
||||||
!enabled({
|
!enabled({
|
||||||
gate,
|
features: ax.features,
|
||||||
currentAccount,
|
currentAccount,
|
||||||
currentProfile,
|
currentProfile,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -165,11 +165,11 @@ function Inner({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
ax.features,
|
||||||
nuxs,
|
nuxs,
|
||||||
snoozed,
|
snoozed,
|
||||||
snoozeNuxDialog,
|
snoozeNuxDialog,
|
||||||
saveNux,
|
saveNux,
|
||||||
gate,
|
|
||||||
currentAccount,
|
currentAccount,
|
||||||
currentProfile,
|
currentProfile,
|
||||||
preferences,
|
preferences,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {type AppBskyActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {type useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
|
||||||
import {type SessionAccount} from '#/state/session'
|
import {type SessionAccount} from '#/state/session'
|
||||||
|
import {type AnalyticsContextType} from '#/analytics'
|
||||||
import {type Geolocation} from '#/geolocation'
|
import {type Geolocation} from '#/geolocation'
|
||||||
|
|
||||||
export type EnabledCheckProps = {
|
export type EnabledCheckProps = {
|
||||||
gate: ReturnType<typeof useGate>
|
features: AnalyticsContextType['features']
|
||||||
currentAccount: SessionAccount
|
currentAccount: SessionAccount
|
||||||
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
currentProfile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
preferences: UsePreferencesQueryResponse
|
preferences: UsePreferencesQueryResponse
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useTranslate} from '#/lib/hooks/useTranslate'
|
import {useTranslate} from '#/lib/hooks/useTranslate'
|
||||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useConvoActive} from '#/state/messages/convo'
|
import {useConvoActive} from '#/state/messages/convo'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
@@ -22,6 +21,7 @@ import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/War
|
|||||||
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {usePromptControl} from '#/components/Prompt'
|
import {usePromptControl} from '#/components/Prompt'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||||
import {hasReachedReactionLimit} from './util'
|
import {hasReachedReactionLimit} from './util'
|
||||||
@@ -34,6 +34,7 @@ export let MessageContextMenu = ({
|
|||||||
children: TriggerProps['children']
|
children: TriggerProps['children']
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const convo = useConvoActive()
|
const convo = useConvoActive()
|
||||||
const deleteControl = usePromptControl()
|
const deleteControl = usePromptControl()
|
||||||
@@ -60,16 +61,12 @@ export let MessageContextMenu = ({
|
|||||||
const onPressTranslateMessage = useCallback(() => {
|
const onPressTranslateMessage = useCallback(() => {
|
||||||
translate(message.text, langPrefs.primaryLanguage)
|
translate(message.text, langPrefs.primaryLanguage)
|
||||||
|
|
||||||
logger.metric(
|
ax.metric('translate', {
|
||||||
'translate',
|
sourceLanguages: [],
|
||||||
{
|
targetLanguage: langPrefs.primaryLanguage,
|
||||||
sourceLanguages: [],
|
textLength: message.text.length,
|
||||||
targetLanguage: langPrefs.primaryLanguage,
|
})
|
||||||
textLength: message.text.length,
|
}, [ax, langPrefs.primaryLanguage, message.text, translate])
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
}, [langPrefs.primaryLanguage, message.text, translate])
|
|
||||||
|
|
||||||
const onDelete = useCallback(() => {
|
const onDelete = useCallback(() => {
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
|
|
||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -15,6 +14,7 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {canBeMessaged} from '#/components/dms/util'
|
import {canBeMessaged} from '#/components/dms/util'
|
||||||
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function MessageProfileButton({
|
export function MessageProfileButton({
|
||||||
profile,
|
profile,
|
||||||
@@ -23,13 +23,14 @@ export function MessageProfileButton({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
|
||||||
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
||||||
const {mutate: initiateConvo} = useGetConvoForMembers({
|
const {mutate: initiateConvo} = useGetConvoForMembers({
|
||||||
onSuccess: ({convo}) => {
|
onSuccess: ({convo}) => {
|
||||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
ax.metric('chat:open', {logContext: 'ProfileHeader'})
|
||||||
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -43,15 +44,15 @@ export function MessageProfileButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (convoAvailability.convo) {
|
if (convoAvailability.convo) {
|
||||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
ax.metric('chat:open', {logContext: 'ProfileHeader'})
|
||||||
navigation.navigate('MessagesConversation', {
|
navigation.navigate('MessagesConversation', {
|
||||||
conversation: convoAvailability.convo.id,
|
conversation: convoAvailability.convo.id,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
ax.metric('chat:create', {logContext: 'ProfileHeader'})
|
||||||
initiateConvo([profile.did])
|
initiateConvo([profile.did])
|
||||||
}
|
}
|
||||||
}, [navigation, profile.did, initiateConvo, convoAvailability])
|
}, [ax, navigation, profile.did, initiateConvo, convoAvailability])
|
||||||
|
|
||||||
const wrappedOnPress = requireEmailVerification(onPress, {
|
const wrappedOnPress = requireEmailVerification(onPress, {
|
||||||
instructions: [
|
instructions: [
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
@@ -12,6 +11,7 @@ import {useTheme} from '#/alf'
|
|||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function NewChat({
|
export function NewChat({
|
||||||
control,
|
control,
|
||||||
@@ -22,6 +22,7 @@ export function NewChat({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
|
||||||
const {mutate: createChat} = useGetConvoForMembers({
|
const {mutate: createChat} = useGetConvoForMembers({
|
||||||
@@ -29,9 +30,9 @@ export function NewChat({
|
|||||||
onNewChat(data.convo.id)
|
onNewChat(data.convo.id)
|
||||||
|
|
||||||
if (!data.convo.lastMessage) {
|
if (!data.convo.lastMessage) {
|
||||||
logEvent('chat:create', {logContext: 'NewChatDialog'})
|
ax.metric('chat:create', {logContext: 'NewChatDialog'})
|
||||||
}
|
}
|
||||||
logEvent('chat:open', {logContext: 'NewChatDialog'})
|
ax.metric('chat:open', {logContext: 'NewChatDialog'})
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
logger.error('Failed to create chat', {safeMessage: error})
|
logger.error('Failed to create chat', {safeMessage: error})
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import {useCallback} from 'react'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function SendViaChatDialog({
|
export function SendViaChatDialog({
|
||||||
control,
|
control,
|
||||||
@@ -32,14 +32,15 @@ function SendViaChatDialogInner({
|
|||||||
onSelectChat: (chatId: string) => void
|
onSelectChat: (chatId: string) => void
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {mutate: createChat} = useGetConvoForMembers({
|
const {mutate: createChat} = useGetConvoForMembers({
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
onSelectChat(data.convo.id)
|
onSelectChat(data.convo.id)
|
||||||
|
|
||||||
if (!data.convo.lastMessage) {
|
if (!data.convo.lastMessage) {
|
||||||
logEvent('chat:create', {logContext: 'SendViaChatDialog'})
|
ax.metric('chat:create', {logContext: 'SendViaChatDialog'})
|
||||||
}
|
}
|
||||||
logEvent('chat:open', {logContext: 'SendViaChatDialog'})
|
ax.metric('chat:open', {logContext: 'SendViaChatDialog'})
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
logger.error('Failed to share post to chat', {message: error})
|
logger.error('Failed to share post to chat', {message: error})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {type FeedPostSliceItem} from '#/state/queries/post-feed'
|
import {type FeedPostSliceItem} from '#/state/queries/post-feed'
|
||||||
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||||
import {atoms as a, useGutters} from '#/alf'
|
import {atoms as a, useGutters} from '#/alf'
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
VideoPostCard,
|
VideoPostCard,
|
||||||
VideoPostCardPlaceholder,
|
VideoPostCardPlaceholder,
|
||||||
} from '#/components/VideoPostCard'
|
} from '#/components/VideoPostCard'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function PostFeedVideoGridRow({
|
export function PostFeedVideoGridRow({
|
||||||
items: slices,
|
items: slices,
|
||||||
@@ -18,6 +18,7 @@ export function PostFeedVideoGridRow({
|
|||||||
items: FeedPostSliceItem[]
|
items: FeedPostSliceItem[]
|
||||||
sourceContext: VideoFeedSourceContext
|
sourceContext: VideoFeedSourceContext
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const gutters = useGutters(['base', 'base', 0, 'base'])
|
const gutters = useGutters(['base', 'base', 0, 'base'])
|
||||||
const posts = slices
|
const posts = slices
|
||||||
.filter(slice => AppBskyEmbedVideo.isView(slice.post.embed))
|
.filter(slice => AppBskyEmbedVideo.isView(slice.post.embed))
|
||||||
@@ -43,7 +44,7 @@ export function PostFeedVideoGridRow({
|
|||||||
sourceContext={sourceContext}
|
sourceContext={sourceContext}
|
||||||
moderation={post.moderation}
|
moderation={post.moderation}
|
||||||
onInteract={() => {
|
onInteract={() => {
|
||||||
logEvent('videoCard:click', {context: 'feed'})
|
ax.metric('videoCard:click', {context: 'feed'})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import React from 'react'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {type LogEvents} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||||
import {useRequireAuth} from '#/state/session'
|
import {useRequireAuth} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import {type Metrics} from '#/analytics/metrics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function useFollowMethods({
|
export function useFollowMethods({
|
||||||
@@ -15,8 +15,8 @@ export function useFollowMethods({
|
|||||||
logContext,
|
logContext,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<bsky.profile.AnyProfileView>
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
logContext: LogEvents['profile:follow']['logContext'] &
|
logContext: Metrics['profile:follow']['logContext'] &
|
||||||
LogEvents['profile:unfollow']['logContext']
|
Metrics['profile:unfollow']['logContext']
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {ScrollView, View} from 'react-native'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {
|
import {
|
||||||
useTrendingSettings,
|
useTrendingSettings,
|
||||||
useTrendingSettingsApi,
|
useTrendingSettingsApi,
|
||||||
@@ -19,6 +18,7 @@ import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Tre
|
|||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {TrendingTopicLink} from '#/components/TrendingTopics'
|
import {TrendingTopicLink} from '#/components/TrendingTopics'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function TrendingInterstitial() {
|
export function TrendingInterstitial() {
|
||||||
const {enabled} = useTrendingConfig()
|
const {enabled} = useTrendingConfig()
|
||||||
@@ -29,6 +29,7 @@ export function TrendingInterstitial() {
|
|||||||
export function Inner() {
|
export function Inner() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const gutters = useGutters([0, 'base', 0, 'base'])
|
const gutters = useGutters([0, 'base', 0, 'base'])
|
||||||
const trendingPrompt = Prompt.usePromptControl()
|
const trendingPrompt = Prompt.usePromptControl()
|
||||||
const {setTrendingDisabled} = useTrendingSettingsApi()
|
const {setTrendingDisabled} = useTrendingSettingsApi()
|
||||||
@@ -36,9 +37,9 @@ export function Inner() {
|
|||||||
const noTopics = !isLoading && !error && !trending?.topics?.length
|
const noTopics = !isLoading && !error && !trending?.topics?.length
|
||||||
|
|
||||||
const onConfirmHide = React.useCallback(() => {
|
const onConfirmHide = React.useCallback(() => {
|
||||||
logEvent('trendingTopics:hide', {context: 'interstitial'})
|
ax.metric('trendingTopics:hide', {context: 'interstitial'})
|
||||||
setTrendingDisabled(true)
|
setTrendingDisabled(true)
|
||||||
}, [setTrendingDisabled])
|
}, [ax, setTrendingDisabled])
|
||||||
|
|
||||||
return error || noTopics ? null : (
|
return error || noTopics ? null : (
|
||||||
<View style={[t.atoms.border_contrast_low, a.border_t, a.border_b]}>
|
<View style={[t.atoms.border_contrast_low, a.border_t, a.border_b]}>
|
||||||
@@ -94,7 +95,9 @@ export function Inner() {
|
|||||||
key={topic.link}
|
key={topic.link}
|
||||||
topic={topic}
|
topic={topic}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logEvent('trendingTopic:click', {context: 'interstitial'})
|
ax.metric('trendingTopic:click', {
|
||||||
|
context: 'interstitial',
|
||||||
|
})
|
||||||
}}>
|
}}>
|
||||||
<View style={[a.py_lg]}>
|
<View style={[a.py_lg]}>
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {VIDEO_FEED_URI} from '#/lib/constants'
|
import {VIDEO_FEED_URI} from '#/lib/constants'
|
||||||
import {makeCustomFeedLink} from '#/lib/routes/links'
|
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {useTrendingSettingsApi} from '#/state/preferences/trending'
|
import {useTrendingSettingsApi} from '#/state/preferences/trending'
|
||||||
import {usePostFeedQuery} from '#/state/queries/post-feed'
|
import {RQKEY, usePostFeedQuery} from '#/state/queries/post-feed'
|
||||||
import {RQKEY} from '#/state/queries/post-feed'
|
|
||||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
@@ -23,6 +21,7 @@ import {
|
|||||||
CompactVideoPostCard,
|
CompactVideoPostCard,
|
||||||
CompactVideoPostCardPlaceholder,
|
CompactVideoPostCardPlaceholder,
|
||||||
} from '#/components/VideoPostCard'
|
} from '#/components/VideoPostCard'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
const CARD_WIDTH = 108
|
const CARD_WIDTH = 108
|
||||||
|
|
||||||
@@ -36,6 +35,7 @@ const FEED_PARAMS: {
|
|||||||
export function TrendingVideos() {
|
export function TrendingVideos() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
const gutters = useGutters([0, 'base'])
|
const gutters = useGutters([0, 'base'])
|
||||||
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
|
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
|
||||||
|
|
||||||
@@ -57,8 +57,8 @@ export function TrendingVideos() {
|
|||||||
|
|
||||||
const onConfirmHide = useCallback(() => {
|
const onConfirmHide = useCallback(() => {
|
||||||
setTrendingVideoDisabled(true)
|
setTrendingVideoDisabled(true)
|
||||||
logEvent('trendingVideos:hide', {context: 'interstitial:discover'})
|
ax.metric('trendingVideos:hide', {context: 'interstitial:discover'})
|
||||||
}, [setTrendingVideoDisabled])
|
}, [ax, setTrendingVideoDisabled])
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return null
|
return null
|
||||||
@@ -147,6 +147,7 @@ function VideoCards({
|
|||||||
}: {
|
}: {
|
||||||
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
|
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
return data.pages
|
return data.pages
|
||||||
.flatMap(page => page.slices)
|
.flatMap(page => page.slices)
|
||||||
@@ -169,7 +170,7 @@ function VideoCards({
|
|||||||
sourceInterstitial: 'discover',
|
sourceInterstitial: 'discover',
|
||||||
}}
|
}}
|
||||||
onInteract={() => {
|
onInteract={() => {
|
||||||
logEvent('videoCard:click', {
|
ax.metric('videoCard:click', {
|
||||||
context: 'interstitial:discover',
|
context: 'interstitial:discover',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
|||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {unstableCacheProfileView} from '#/state/queries/profile'
|
import {unstableCacheProfileView} from '#/state/queries/profile'
|
||||||
import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
||||||
@@ -22,6 +21,7 @@ import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
|
|||||||
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
|
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {Globe_Stroke2_Corner0_Rounded} from '../icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded} from '../icons/Globe'
|
||||||
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '../icons/SquareArrowTopRight'
|
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '../icons/SquareArrowTopRight'
|
||||||
@@ -103,6 +103,7 @@ export function LiveStatus({
|
|||||||
padding?: 'lg' | 'xl'
|
padding?: 'lg' | 'xl'
|
||||||
onPressOpenProfile: () => void
|
onPressOpenProfile: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -174,11 +175,7 @@ export function LiveStatus({
|
|||||||
color="primary"
|
color="primary"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric(
|
ax.metric('live:card:watch', {subject: profile.did})
|
||||||
'live:card:watch',
|
|
||||||
{subject: profile.did},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
openLink(embed.external.uri, false)
|
openLink(embed.external.uri, false)
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
@@ -207,11 +204,7 @@ export function LiveStatus({
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric(
|
ax.metric('live:card:openProfile', {subject: profile.did})
|
||||||
'live:card:openProfile',
|
|
||||||
{subject: profile.did},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
unstableCacheProfileView(queryClient, profile)
|
unstableCacheProfileView(queryClient, profile)
|
||||||
onPressOpenProfile()
|
onPressOpenProfile()
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {uploadBlob} from '#/lib/api'
|
import {uploadBlob} from '#/lib/api'
|
||||||
import {imageToThumb} from '#/lib/api/resolve'
|
import {imageToThumb} from '#/lib/api/resolve'
|
||||||
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
|
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useLiveNowConfig} from '#/state/service-config'
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useDialogContext} from '#/components/Dialog'
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
import {getLiveServiceNames} from '#/components/live/utils'
|
import {getLiveServiceNames} from '#/components/live/utils'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function useLiveLinkMetaQuery(url: string | null) {
|
export function useLiveLinkMetaQuery(url: string | null) {
|
||||||
const liveNowConfig = useLiveNowConfig()
|
const liveNowConfig = useLiveNowConfig()
|
||||||
@@ -50,6 +50,7 @@ export function useUpsertLiveStatusMutation(
|
|||||||
linkMeta: LinkMeta | null | undefined,
|
linkMeta: LinkMeta | null | undefined,
|
||||||
createdAt?: string,
|
createdAt?: string,
|
||||||
) {
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -77,7 +78,7 @@ export function useUpsertLiveStatusMutation(
|
|||||||
thumb = blob.data.blob
|
thumb = blob.data.blob
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Failed to upload thumbnail for live status`, {
|
ax.logger.error(`Failed to upload thumbnail for live status`, {
|
||||||
url: linkMeta.url,
|
url: linkMeta.url,
|
||||||
image: linkMeta.image,
|
image: linkMeta.image,
|
||||||
safeMessage: e,
|
safeMessage: e,
|
||||||
@@ -133,7 +134,7 @@ export function useUpsertLiveStatusMutation(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (e: any) => {
|
onError: (e: any) => {
|
||||||
logger.error(`Failed to upsert live status`, {
|
ax.logger.error(`Failed to upsert live status`, {
|
||||||
url: linkMeta?.url,
|
url: linkMeta?.url,
|
||||||
image: linkMeta?.image,
|
image: linkMeta?.image,
|
||||||
safeMessage: e,
|
safeMessage: e,
|
||||||
@@ -141,17 +142,9 @@ export function useUpsertLiveStatusMutation(
|
|||||||
},
|
},
|
||||||
onSuccess: ({record, image}) => {
|
onSuccess: ({record, image}) => {
|
||||||
if (createdAt) {
|
if (createdAt) {
|
||||||
logger.metric(
|
ax.metric('live:edit', {duration: record.durationMinutes})
|
||||||
'live:edit',
|
|
||||||
{duration: record.durationMinutes},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
logger.metric(
|
ax.metric('live:create', {duration: record.durationMinutes})
|
||||||
'live:create',
|
|
||||||
{duration: record.durationMinutes},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.show(_(msg`You are now live!`))
|
Toast.show(_(msg`You are now live!`))
|
||||||
@@ -187,6 +180,7 @@ export function useUpsertLiveStatusMutation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRemoveLiveStatusMutation() {
|
export function useRemoveLiveStatusMutation() {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -203,12 +197,12 @@ export function useRemoveLiveStatusMutation() {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (e: any) => {
|
onError: (e: any) => {
|
||||||
logger.error(`Failed to remove live status`, {
|
ax.logger.error(`Failed to remove live status`, {
|
||||||
safeMessage: e,
|
safeMessage: e,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
logger.metric('live:remove', {}, {statsig: true})
|
ax.metric('live:remove', {})
|
||||||
Toast.show(_(msg`You are no longer live`))
|
Toast.show(_(msg`You are no longer live`))
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
if (!currentAccount) return
|
if (!currentAccount) return
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {wait} from '#/lib/async/wait'
|
import {wait} from '#/lib/async/wait'
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {Logger} from '#/logger'
|
|
||||||
import {useMyLabelersQuery} from '#/state/queries/preferences'
|
import {useMyLabelersQuery} from '#/state/queries/preferences'
|
||||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
@@ -28,6 +28,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
|||||||
import {createStaticClick, InlineLinkText, Link} from '#/components/Link'
|
import {createStaticClick, InlineLinkText, Link} from '#/components/Link'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {useSubmitReportMutation} from './action'
|
import {useSubmitReportMutation} from './action'
|
||||||
import {
|
import {
|
||||||
@@ -53,8 +54,6 @@ export function useGlobalReportDialogControl() {
|
|||||||
return useGlobalDialogsControlContext().reportDialogControl
|
return useGlobalDialogsControlContext().reportDialogControl
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.ReportDialog)
|
|
||||||
|
|
||||||
export function GlobalReportDialog() {
|
export function GlobalReportDialog() {
|
||||||
const {value, control} = useGlobalReportDialogControl()
|
const {value, control} = useGlobalReportDialogControl()
|
||||||
return <ReportDialog control={control} subject={value?.subject} />
|
return <ReportDialog control={control} subject={value?.subject} />
|
||||||
@@ -65,13 +64,14 @@ export function ReportDialog(
|
|||||||
subject?: ReportSubject
|
subject?: ReportSubject
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const subject = React.useMemo(
|
const subject = React.useMemo(
|
||||||
() => (props.subject ? parseReportSubject(props.subject) : undefined),
|
() => (props.subject ? parseReportSubject(props.subject) : undefined),
|
||||||
[props.subject],
|
[props.subject],
|
||||||
)
|
)
|
||||||
const onClose = React.useCallback(() => {
|
const onClose = React.useCallback(() => {
|
||||||
logger.metric('reportDialog:close', {}, {statsig: false})
|
ax.metric('reportDialog:close', {})
|
||||||
}, [])
|
}, [ax])
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control} onClose={onClose}>
|
<Dialog.Outer control={props.control} onClose={onClose}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
@@ -103,6 +103,8 @@ function Invalid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Inner(props: ReportDialogProps) {
|
function Inner(props: ReportDialogProps) {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const ref = React.useRef<ScrollView>(null)
|
const ref = React.useRef<ScrollView>(null)
|
||||||
@@ -208,15 +210,11 @@ function Inner(props: ReportDialogProps) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
logger.metric(
|
ax.metric('reportDialog:success', {
|
||||||
'reportDialog:success',
|
reason: state.selectedOption?.reason ?? '',
|
||||||
{
|
labeler: state.selectedLabeler?.creator.handle ?? '',
|
||||||
reason: state.selectedOption?.reason ?? '',
|
details: !!state.details,
|
||||||
labeler: state.selectedLabeler?.creator.handle ?? '',
|
})
|
||||||
details: !!state.details,
|
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
// give time for user feedback
|
// give time for user feedback
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.control.close(() => {
|
props.control.close(() => {
|
||||||
@@ -224,7 +222,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
})
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.metric('reportDialog:failure', {}, {statsig: false})
|
ax.metric('reportDialog:failure', {})
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
source: 'ReportDialog',
|
source: 'ReportDialog',
|
||||||
})
|
})
|
||||||
@@ -237,15 +235,11 @@ function Inner(props: ReportDialogProps) {
|
|||||||
}
|
}
|
||||||
}, [_, submitReport, state, dispatch, props, setPending, setSuccess])
|
}, [_, submitReport, state, dispatch, props, setPending, setSuccess])
|
||||||
|
|
||||||
React.useEffect(() => {
|
useCallOnce(() => {
|
||||||
logger.metric(
|
ax.metric('reportDialog:open', {
|
||||||
'reportDialog:open',
|
subjectType: props.subject.type,
|
||||||
{
|
})
|
||||||
subjectType: props.subject.type,
|
})()
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
}, [props.subject])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {View} from 'react-native'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
@@ -12,6 +11,7 @@ import {type FullVerificationState} from '#/components/verification'
|
|||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {VerificationsDialog} from '#/components/verification/VerificationsDialog'
|
import {VerificationsDialog} from '#/components/verification/VerificationsDialog'
|
||||||
import {VerifierDialog} from '#/components/verification/VerifierDialog'
|
import {VerifierDialog} from '#/components/verification/VerifierDialog'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function shouldShowVerificationCheckButton(
|
export function shouldShowVerificationCheckButton(
|
||||||
@@ -77,6 +77,7 @@ export function Badge({
|
|||||||
size: 'lg' | 'md' | 'sm'
|
size: 'lg' | 'md' | 'sm'
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const verificationsDialogControl = useDialogControl()
|
const verificationsDialogControl = useDialogControl()
|
||||||
const verifierDialogControl = useDialogControl()
|
const verifierDialogControl = useDialogControl()
|
||||||
@@ -101,7 +102,7 @@ export function Badge({
|
|||||||
hitSlop={20}
|
hitSlop={20}
|
||||||
onPress={evt => {
|
onPress={evt => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
logger.metric('verification:badge:click', {}, {statsig: true})
|
ax.metric('verification:badge:click', {})
|
||||||
if (state.profile.role === 'verifier') {
|
if (state.profile.role === 'verifier') {
|
||||||
verifierDialogControl.open()
|
verifierDialogControl.open()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {urls} from '#/lib/constants'
|
import {urls} from '#/lib/constants'
|
||||||
import {getUserDisplayName} from '#/lib/getUserDisplayName'
|
import {getUserDisplayName} from '#/lib/getUserDisplayName'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
@@ -20,6 +19,7 @@ import * as ProfileCard from '#/components/ProfileCard'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {type FullVerificationState} from '#/components/verification'
|
import {type FullVerificationState} from '#/components/verification'
|
||||||
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
|
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export {useDialogControl} from '#/components/Dialog'
|
export {useDialogControl} from '#/components/Dialog'
|
||||||
@@ -55,6 +55,7 @@ function Inner({
|
|||||||
verificationState: FullVerificationState
|
verificationState: FullVerificationState
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
@@ -158,13 +159,9 @@ function Inner({
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
style={[a.justify_center]}
|
style={[a.justify_center]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric(
|
ax.metric('verification:learn-more', {
|
||||||
'verification:learn-more',
|
location: 'verificationsDialog',
|
||||||
{
|
})
|
||||||
location: 'verificationsDialog',
|
|
||||||
},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans context="english-only-resource">Learn more</Trans>
|
<Trans context="english-only-resource">Learn more</Trans>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {urls} from '#/lib/constants'
|
import {urls} from '#/lib/constants'
|
||||||
import {getUserDisplayName} from '#/lib/getUserDisplayName'
|
import {getUserDisplayName} from '#/lib/getUserDisplayName'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
@@ -14,6 +13,7 @@ import {VerifierCheck} from '#/components/icons/VerifierCheck'
|
|||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {type FullVerificationState} from '#/components/verification'
|
import {type FullVerificationState} from '#/components/verification'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export {useDialogControl} from '#/components/Dialog'
|
export {useDialogControl} from '#/components/Dialog'
|
||||||
@@ -49,6 +49,7 @@ function Inner({
|
|||||||
verificationState: FullVerificationState
|
verificationState: FullVerificationState
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -126,13 +127,9 @@ function Inner({
|
|||||||
color="primary"
|
color="primary"
|
||||||
style={[a.justify_center]}
|
style={[a.justify_center]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric(
|
ax.metric('verification:learn-more', {
|
||||||
'verification:learn-more',
|
location: 'verifierDialog',
|
||||||
{
|
})
|
||||||
location: 'verifierDialog',
|
|
||||||
},
|
|
||||||
{statsig: true},
|
|
||||||
)
|
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans context="english-only-resource">Learn more</Trans>
|
<Trans context="english-only-resource">Learn more</Trans>
|
||||||
|
|||||||
Vendored
+19
-1
@@ -50,7 +50,7 @@ export const BUNDLE_IDENTIFIER: string =
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This will always be in the format of YYMMDDHH, so that it always increases
|
* This will always be in the format of YYMMDDHH, so that it always increases
|
||||||
* for each build. This should only be used for StatSig reporting and shouldn't
|
* for each build. This should only be used for analytics reporting and shouldn't
|
||||||
* be used to identify a specific bundle.
|
* be used to identify a specific bundle.
|
||||||
*/
|
*/
|
||||||
export const BUNDLE_DATE: number =
|
export const BUNDLE_DATE: number =
|
||||||
@@ -84,6 +84,24 @@ export const BLUESKY_PROXY_DID: Did =
|
|||||||
export const CHAT_PROXY_DID: Did =
|
export const CHAT_PROXY_DID: Did =
|
||||||
process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'
|
process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics API host
|
||||||
|
*/
|
||||||
|
export const METRICS_API_HOST: string =
|
||||||
|
process.env.EXPO_PUBLIC_METRICS_API_HOST || 'https://events.bsky.app'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Growthbook API host
|
||||||
|
*/
|
||||||
|
export const GROWTHBOOK_API_HOST: string =
|
||||||
|
process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST || `${METRICS_API_HOST}/gb`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Growthbook client key
|
||||||
|
*/
|
||||||
|
export const GROWTHBOOK_CLIENT_KEY: string =
|
||||||
|
process.env.EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY || 'sdk-7gkUkGy9wguUjyFe'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentry DSN for telemetry
|
* Sentry DSN for telemetry
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers'
|
import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {atoms as a, utils} from '#/alf'
|
import {atoms as a, utils} from '#/alf'
|
||||||
import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live'
|
import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {
|
import {
|
||||||
type LiveEventFeed,
|
type LiveEventFeed,
|
||||||
type LiveEventFeedMetricContext,
|
type LiveEventFeedMetricContext,
|
||||||
@@ -26,6 +26,7 @@ export function LiveEventFeedCardCompact({
|
|||||||
metricContext: LiveEventFeedMetricContext
|
metricContext: LiveEventFeedMetricContext
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
|
||||||
const layout = feed.layouts.compact
|
const layout = feed.layouts.compact
|
||||||
const overlayColor = layout.overlayColor
|
const overlayColor = layout.overlayColor
|
||||||
@@ -39,7 +40,7 @@ export function LiveEventFeedCardCompact({
|
|||||||
}, [feed.url])
|
}, [feed.url])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.metric('liveEvents:feedBanner:seen', {
|
ax.metric('liveEvents:feedBanner:seen', {
|
||||||
feed: feed.url,
|
feed: feed.url,
|
||||||
context: metricContext,
|
context: metricContext,
|
||||||
})
|
})
|
||||||
@@ -52,7 +53,7 @@ export function LiveEventFeedCardCompact({
|
|||||||
label={_(msg`Live event happening now: ${feed.title}`)}
|
label={_(msg`Live event happening now: ${feed.title}`)}
|
||||||
style={[a.w_full]}
|
style={[a.w_full]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('liveEvents:feedBanner:click', {
|
ax.metric('liveEvents:feedBanner:click', {
|
||||||
feed: feed.url,
|
feed: feed.url,
|
||||||
context: metricContext,
|
context: metricContext,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import {useEffect, useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers'
|
import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {atoms as a, useBreakpoints, utils} from '#/alf'
|
import {atoms as a, useBreakpoints, utils} from '#/alf'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {
|
import {
|
||||||
type LiveEventFeed,
|
type LiveEventFeed,
|
||||||
type LiveEventFeedMetricContext,
|
type LiveEventFeedMetricContext,
|
||||||
@@ -24,6 +25,7 @@ export function LiveEventFeedCardWide({
|
|||||||
feed: LiveEventFeed
|
feed: LiveEventFeed
|
||||||
metricContext: LiveEventFeedMetricContext
|
metricContext: LiveEventFeedMetricContext
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
|
|
||||||
@@ -38,13 +40,12 @@ export function LiveEventFeedCardWide({
|
|||||||
return '/'
|
return '/'
|
||||||
}, [feed.url])
|
}, [feed.url])
|
||||||
|
|
||||||
useEffect(() => {
|
useCallOnce(() => {
|
||||||
logger.metric('liveEvents:feedBanner:seen', {
|
ax.metric('liveEvents:feedBanner:seen', {
|
||||||
feed: feed.url,
|
feed: feed.url,
|
||||||
context: metricContext,
|
context: metricContext,
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
})()
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -52,7 +53,7 @@ export function LiveEventFeedCardWide({
|
|||||||
label={_(msg`Live event happening now: ${feed.title}`)}
|
label={_(msg`Live event happening now: ${feed.title}`)}
|
||||||
style={[a.w_full]}
|
style={[a.w_full]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
logger.metric('liveEvents:feedBanner:click', {
|
ax.metric('liveEvents:feedBanner:click', {
|
||||||
feed: feed.url,
|
feed: feed.url,
|
||||||
context: metricContext,
|
context: metricContext,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import {useEffect} from 'react'
|
|||||||
import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api'
|
import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api'
|
||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {
|
import {
|
||||||
preferencesQueryKey,
|
preferencesQueryKey,
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import * as env from '#/env'
|
import * as env from '#/env'
|
||||||
import {
|
import {
|
||||||
@@ -63,6 +63,7 @@ export function useUpdateLiveEventPreferences(props: {
|
|||||||
undoAction: LiveEventPreferencesAction | null
|
undoAction: LiveEventPreferencesAction | null
|
||||||
}) => void
|
}) => void
|
||||||
}) {
|
}) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ export function useUpdateLiveEventPreferences(props: {
|
|||||||
case 'hideFeed':
|
case 'hideFeed':
|
||||||
case 'unhideFeed': {
|
case 'unhideFeed': {
|
||||||
if (!props.feed) {
|
if (!props.feed) {
|
||||||
logger.error(
|
ax.logger.error(
|
||||||
`useUpdateLiveEventPreferences: feed is missing, but required for hiding/unhiding`,
|
`useUpdateLiveEventPreferences: feed is missing, but required for hiding/unhiding`,
|
||||||
{
|
{
|
||||||
action,
|
action,
|
||||||
@@ -125,7 +126,7 @@ export function useUpdateLiveEventPreferences(props: {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.metric(
|
ax.metric(
|
||||||
action.type === 'hideFeed'
|
action.type === 'hideFeed'
|
||||||
? 'liveEvents:feedBanner:hide'
|
? 'liveEvents:feedBanner:hide'
|
||||||
: 'liveEvents:feedBanner:unhide',
|
: 'liveEvents:feedBanner:unhide',
|
||||||
@@ -138,11 +139,11 @@ export function useUpdateLiveEventPreferences(props: {
|
|||||||
}
|
}
|
||||||
case 'toggleHideAllFeeds': {
|
case 'toggleHideAllFeeds': {
|
||||||
if (prefs!.hideAllFeeds) {
|
if (prefs!.hideAllFeeds) {
|
||||||
logger.metric('liveEvents:hideAllFeedBanners', {
|
ax.metric('liveEvents:hideAllFeedBanners', {
|
||||||
context: props.metricContext,
|
context: props.metricContext,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger.metric('liveEvents:unhideAllFeedBanners', {
|
ax.metric('liveEvents:unhideAllFeedBanners', {
|
||||||
context: props.metricContext,
|
context: props.metricContext,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import {AppState, type AppStateStatus} from 'react-native'
|
||||||
|
|
||||||
|
export const getCurrentState = () => AppState.currentState
|
||||||
|
|
||||||
|
export function onAppStateChange(cb: (state: AppStateStatus) => void) {
|
||||||
|
let prev = AppState.currentState
|
||||||
|
return AppState.addEventListener('change', next => {
|
||||||
|
if (next === prev) return
|
||||||
|
prev = next
|
||||||
|
cb(next)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppState() {
|
||||||
|
const [state, setState] = useState(AppState.currentState)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = onAppStateChange(next => {
|
||||||
|
setState(next)
|
||||||
|
})
|
||||||
|
return () => sub.remove()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
@@ -6,11 +6,12 @@ import {logger} from '#/logger'
|
|||||||
import {type SessionAccount, useSessionApi} from '#/state/session'
|
import {type SessionAccount, useSessionApi} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {type Metrics} from '#/analytics/metrics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import {logEvent} from '../statsig/statsig'
|
|
||||||
import {type LogEvents} from '../statsig/statsig'
|
|
||||||
|
|
||||||
export function useAccountSwitcher() {
|
export function useAccountSwitcher() {
|
||||||
|
const ax = useAnalytics()
|
||||||
const [pendingDid, setPendingDid] = useState<string | null>(null)
|
const [pendingDid, setPendingDid] = useState<string | null>(null)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {resumeSession} = useSessionApi()
|
const {resumeSession} = useSessionApi()
|
||||||
@@ -19,7 +20,7 @@ export function useAccountSwitcher() {
|
|||||||
const onPressSwitchAccount = useCallback(
|
const onPressSwitchAccount = useCallback(
|
||||||
async (
|
async (
|
||||||
account: SessionAccount,
|
account: SessionAccount,
|
||||||
logContext: LogEvents['account:loggedIn']['logContext'],
|
logContext: Metrics['account:loggedIn']['logContext'],
|
||||||
) => {
|
) => {
|
||||||
if (pendingDid) {
|
if (pendingDid) {
|
||||||
// The session API isn't resilient to race conditions so let's just ignore this.
|
// The session API isn't resilient to race conditions so let's just ignore this.
|
||||||
@@ -37,7 +38,7 @@ export function useAccountSwitcher() {
|
|||||||
history.pushState(null, '', '/')
|
history.pushState(null, '', '/')
|
||||||
}
|
}
|
||||||
await resumeSession(account, true)
|
await resumeSession(account, true)
|
||||||
logEvent('account:loggedIn', {logContext, withPassword: false})
|
ax.metric('account:loggedIn', {logContext, withPassword: false})
|
||||||
Toast.show(_(msg`Signed in as @${account.handle}`))
|
Toast.show(_(msg`Signed in as @${account.handle}`))
|
||||||
} else {
|
} else {
|
||||||
requestSwitchToAccount({requestedAccount: account.did})
|
requestSwitchToAccount({requestedAccount: account.did})
|
||||||
@@ -59,7 +60,7 @@ export function useAccountSwitcher() {
|
|||||||
setPendingDid(null)
|
setPendingDid(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_, resumeSession, requestSwitchToAccount, pendingDid],
|
[_, ax, resumeSession, requestSwitchToAccount, pendingDid],
|
||||||
)
|
)
|
||||||
|
|
||||||
return {onPressSwitchAccount, pendingDid}
|
return {onPressSwitchAccount, pendingDid}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {AppState} from 'react-native'
|
|
||||||
|
|
||||||
export function useAppState() {
|
|
||||||
const [state, setState] = useState(AppState.currentState)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sub = AppState.addEventListener('change', nextAppState => {
|
|
||||||
setState(nextAppState)
|
|
||||||
})
|
|
||||||
return () => sub.remove()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import {useCallback} from 'react'
|
|
||||||
|
|
||||||
export enum OnceKey {
|
|
||||||
PreferencesThread = 'preferences:thread',
|
|
||||||
}
|
|
||||||
|
|
||||||
const called: Record<OnceKey, boolean> = {
|
|
||||||
[OnceKey.PreferencesThread]: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCallOnce(key: OnceKey) {
|
|
||||||
return useCallback(
|
|
||||||
(cb: () => void) => {
|
|
||||||
if (called[key] === true) return
|
|
||||||
called[key] = true
|
|
||||||
cb()
|
|
||||||
},
|
|
||||||
[key],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,10 @@ import * as WebBrowser from 'expo-web-browser'
|
|||||||
|
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
|
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS, IS_NATIVE} from '#/env'
|
import {IS_IOS, IS_NATIVE} from '#/env'
|
||||||
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
||||||
import {useApplyPullRequestOTAUpdate} from './useOTAUpdates'
|
import {useApplyPullRequestOTAUpdate} from './useOTAUpdates'
|
||||||
@@ -22,6 +22,7 @@ let previousIntentUrl = ''
|
|||||||
|
|
||||||
export function useIntentHandler() {
|
export function useIntentHandler() {
|
||||||
const incomingUrl = Linking.useLinkingURL()
|
const incomingUrl = Linking.useLinkingURL()
|
||||||
|
const ax = useAnalytics()
|
||||||
const composeIntent = useComposeIntent()
|
const composeIntent = useComposeIntent()
|
||||||
const verifyEmailIntent = useVerifyEmailIntent()
|
const verifyEmailIntent = useVerifyEmailIntent()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -36,7 +37,7 @@ export function useIntentHandler() {
|
|||||||
|
|
||||||
const referrerInfo = Referrer.getReferrerInfo()
|
const referrerInfo = Referrer.getReferrerInfo()
|
||||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||||
logger.metric('deepLink:referrerReceived', {
|
ax.metric('deepLink:referrerReceived', {
|
||||||
to: url,
|
to: url,
|
||||||
referrer: referrerInfo?.referrer,
|
referrer: referrerInfo?.referrer,
|
||||||
hostname: referrerInfo?.hostname,
|
hostname: referrerInfo?.hostname,
|
||||||
@@ -95,6 +96,7 @@ export function useIntentHandler() {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
incomingUrl,
|
incomingUrl,
|
||||||
|
ax,
|
||||||
composeIntent,
|
composeIntent,
|
||||||
verifyEmailIntent,
|
verifyEmailIntent,
|
||||||
currentAccount,
|
currentAccount,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
export function useIsBskyTeam() {
|
export function useIsBskyTeam() {
|
||||||
const gate = useGate()
|
const ax = useAnalytics()
|
||||||
return useMemo(() => gate('is_bsky_team_member'), [gate])
|
return useMemo(() => ax.features.enabled(ax.features.IsBskyTeam), [ax])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_ANDROID, IS_IOS} from '#/env'
|
import {IS_ANDROID, IS_IOS} from '#/env'
|
||||||
import {resetToTab} from '#/Navigation'
|
import {resetToTab} from '#/Navigation'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
@@ -75,6 +76,8 @@ let storedAccountSwitchPayload: NotificationPayload
|
|||||||
let lastHandledNotificationDateDedupe = 0
|
let lastHandledNotificationDateDedupe = 0
|
||||||
|
|
||||||
export function useNotificationsHandler() {
|
export function useNotificationsHandler() {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const logger = ax.logger.useChild(ax.logger.Context.Notifications)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
const {onPressSwitchAccount} = useAccountSwitcher()
|
const {onPressSwitchAccount} = useAccountSwitcher()
|
||||||
@@ -190,7 +193,7 @@ export function useNotificationsHandler() {
|
|||||||
if (!payload) return
|
if (!payload) return
|
||||||
|
|
||||||
if (payload.reason === 'chat-message') {
|
if (payload.reason === 'chat-message') {
|
||||||
notyLogger.debug(`useNotificationsHandler: handling chat message`, {
|
logger.debug(`useNotificationsHandler: handling chat message`, {
|
||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -250,7 +253,7 @@ export function useNotificationsHandler() {
|
|||||||
const [screen, params] = router.matchPath(url)
|
const [screen, params] = router.matchPath(url)
|
||||||
// @ts-expect-error router is not typed :/ -sfn
|
// @ts-expect-error router is not typed :/ -sfn
|
||||||
navigation.navigate('HomeTab', {screen, params})
|
navigation.navigate('HomeTab', {screen, params})
|
||||||
notyLogger.debug(`useNotificationsHandler: navigate`, {
|
logger.debug(`useNotificationsHandler: navigate`, {
|
||||||
screen,
|
screen,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -264,7 +267,7 @@ export function useNotificationsHandler() {
|
|||||||
|
|
||||||
if (!payload) return DEFAULT_HANDLER_OPTIONS
|
if (!payload) return DEFAULT_HANDLER_OPTIONS
|
||||||
|
|
||||||
notyLogger.debug('useNotificationsHandler: incoming', {e, payload})
|
logger.debug('useNotificationsHandler: incoming', {e, payload})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
payload.reason === 'chat-message' &&
|
payload.reason === 'chat-message' &&
|
||||||
@@ -290,7 +293,7 @@ export function useNotificationsHandler() {
|
|||||||
if (e.notification.date === lastHandledNotificationDateDedupe) return
|
if (e.notification.date === lastHandledNotificationDateDedupe) return
|
||||||
lastHandledNotificationDateDedupe = e.notification.date
|
lastHandledNotificationDateDedupe = e.notification.date
|
||||||
|
|
||||||
notyLogger.debug('useNotificationsHandler: response received', {
|
logger.debug('useNotificationsHandler: response received', {
|
||||||
actionIdentifier: e.actionIdentifier,
|
actionIdentifier: e.actionIdentifier,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -301,15 +304,14 @@ export function useNotificationsHandler() {
|
|||||||
const payload = getNotificationPayload(e.notification)
|
const payload = getNotificationPayload(e.notification)
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
notyLogger.debug(
|
logger.debug(
|
||||||
'User pressed a notification, opening notifications tab',
|
'User pressed a notification, opening notifications tab',
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
notyLogger.metric(
|
ax.metric('notifications:openApp', {
|
||||||
'notifications:openApp',
|
reason: payload.reason,
|
||||||
{reason: payload.reason, causedBoot: false},
|
causedBoot: false,
|
||||||
{statsig: false},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
invalidateCachedUnreadPage()
|
invalidateCachedUnreadPage()
|
||||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||||
@@ -322,7 +324,7 @@ export function useNotificationsHandler() {
|
|||||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||||
}
|
}
|
||||||
|
|
||||||
notyLogger.debug('Notifications: handleNotification', {
|
logger.debug('Notifications: handleNotification', {
|
||||||
content: e.notification.request.content,
|
content: e.notification.request.content,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
})
|
})
|
||||||
@@ -330,7 +332,7 @@ export function useNotificationsHandler() {
|
|||||||
handleNotification(payload)
|
handleNotification(payload)
|
||||||
Notifications.dismissAllNotificationsAsync()
|
Notifications.dismissAllNotificationsAsync()
|
||||||
} else {
|
} else {
|
||||||
notyLogger.error('useNotificationsHandler: received no payload', {
|
logger.error('useNotificationsHandler: received no payload', {
|
||||||
identifier: e.notification.request.identifier,
|
identifier: e.notification.request.identifier,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -350,6 +352,8 @@ export function useNotificationsHandler() {
|
|||||||
responseReceivedListener.remove()
|
responseReceivedListener.remove()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
ax,
|
||||||
|
logger,
|
||||||
queryClient,
|
queryClient,
|
||||||
currentAccount,
|
currentAccount,
|
||||||
currentConvoId,
|
currentConvoId,
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
|
|
||||||
import {isNetworkError} from '#/lib/strings/errors'
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {IS_ANDROID, IS_IOS} from '#/env'
|
import {IS_ANDROID, IS_IOS, IS_TESTFLIGHT} from '#/env'
|
||||||
import {IS_TESTFLIGHT} from '#/env'
|
|
||||||
|
|
||||||
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
|
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
|
||||||
|
|
||||||
@@ -170,7 +169,7 @@ export function useOTAUpdates() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use this setTimeout to allow Statsig to initialize before we check for an update
|
// We use this setTimeout to allow analytics to initialize before we check for an update
|
||||||
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
|
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
|
||||||
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
|
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
|
||||||
// immediately.
|
// immediately.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {useCallback} from 'react'
|
|||||||
import {Linking} from 'react-native'
|
import {Linking} from 'react-native'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {
|
import {
|
||||||
createBskyAppAbsoluteUrl,
|
createBskyAppAbsoluteUrl,
|
||||||
createProxiedUrl,
|
createProxiedUrl,
|
||||||
@@ -16,9 +15,11 @@ import {useInAppBrowser} from '#/state/preferences/in-app-browser'
|
|||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {useDialogContext} from '#/components/Dialog'
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
|
|
||||||
export function useOpenLink() {
|
export function useOpenLink() {
|
||||||
|
const ax = useAnalytics()
|
||||||
const enabled = useInAppBrowser()
|
const enabled = useInAppBrowser()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const dialogContext = useDialogContext()
|
const dialogContext = useDialogContext()
|
||||||
@@ -31,7 +32,7 @@ export function useOpenLink() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isBskyAppUrl(url)) {
|
if (!isBskyAppUrl(url)) {
|
||||||
logEvent('link:clicked', {
|
ax.metric('link:clicked', {
|
||||||
domain: toNiceDomain(url),
|
domain: toNiceDomain(url),
|
||||||
url,
|
url,
|
||||||
})
|
})
|
||||||
@@ -72,7 +73,7 @@ export function useOpenLink() {
|
|||||||
}
|
}
|
||||||
Linking.openURL(url)
|
Linking.openURL(url)
|
||||||
},
|
},
|
||||||
[enabled, inAppBrowserConsentControl, t, dialogContext],
|
[ax, enabled, inAppBrowserConsentControl, t, dialogContext],
|
||||||
)
|
)
|
||||||
|
|
||||||
return openLink
|
return openLink
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import {useCallback, useRef} from 'react'
|
import {useCallback, useRef} from 'react'
|
||||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {type Metrics, useAnalytics} from '#/analytics'
|
||||||
import {type MetricEvents} from '#/logger/metrics'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that returns a callback to track post:view events.
|
* Hook that returns a callback to track post:view events.
|
||||||
@@ -12,8 +11,9 @@ import {type MetricEvents} from '#/logger/metrics'
|
|||||||
* @returns A callback that accepts a post and logs the view event
|
* @returns A callback that accepts a post and logs the view event
|
||||||
*/
|
*/
|
||||||
export function usePostViewTracking(
|
export function usePostViewTracking(
|
||||||
logContext: MetricEvents['post:view']['logContext'],
|
logContext: Metrics['post:view']['logContext'],
|
||||||
) {
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
const seenUrisRef = useRef(new Set<string>())
|
const seenUrisRef = useRef(new Set<string>())
|
||||||
|
|
||||||
const trackPostView = useCallback(
|
const trackPostView = useCallback(
|
||||||
@@ -21,17 +21,13 @@ export function usePostViewTracking(
|
|||||||
if (seenUrisRef.current.has(post.uri)) return
|
if (seenUrisRef.current.has(post.uri)) return
|
||||||
seenUrisRef.current.add(post.uri)
|
seenUrisRef.current.add(post.uri)
|
||||||
|
|
||||||
logger.metric(
|
ax.metric('post:view', {
|
||||||
'post:view',
|
uri: post.uri,
|
||||||
{
|
authorDid: post.author.did,
|
||||||
uri: post.uri,
|
logContext,
|
||||||
authorDid: post.author.did,
|
})
|
||||||
logContext,
|
|
||||||
},
|
|
||||||
{statsig: false},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
[logContext],
|
[ax, logContext],
|
||||||
)
|
)
|
||||||
|
|
||||||
return trackPostView
|
return trackPostView
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import {useCallback, useEffect} from 'react'
|
|||||||
import {Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
import * as Notifications from 'expo-notifications'
|
import * as Notifications from 'expo-notifications'
|
||||||
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
|
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
|
||||||
import {type AtpAgent} from '@atproto/api'
|
import {type AppBskyNotificationRegisterPush, type AtpAgent} from '@atproto/api'
|
||||||
import {type AppBskyNotificationRegisterPush} from '@atproto/api'
|
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -16,8 +15,8 @@ import {isNetworkError} from '#/lib/strings/errors'
|
|||||||
import {type SessionAccount, useAgent, useSession} from '#/state/session'
|
import {type SessionAccount, useAgent, useSession} from '#/state/session'
|
||||||
import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
|
import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_DEV} from '#/env'
|
import {IS_DEV, IS_NATIVE} from '#/env'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -227,6 +226,7 @@ export function useNotificationsRegistration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRequestNotificationsPermission() {
|
export function useRequestNotificationsPermission() {
|
||||||
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const getAndRegisterPushToken = useGetAndRegisterPushToken()
|
const getAndRegisterPushToken = useGetAndRegisterPushToken()
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ export function useRequestNotificationsPermission() {
|
|||||||
|
|
||||||
const res = await Notifications.requestPermissionsAsync()
|
const res = await Notifications.requestPermissionsAsync()
|
||||||
|
|
||||||
notyLogger.metric(`notifications:request`, {
|
ax.metric(`notifications:request`, {
|
||||||
context: context,
|
context: context,
|
||||||
status: res.status,
|
status: res.status,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {useCallback, useRef} from 'react'
|
||||||
|
|
||||||
|
type Cb = () => void
|
||||||
|
|
||||||
|
export function callOnce() {
|
||||||
|
let ran = false
|
||||||
|
return function runCallbackOnce(cb: Cb) {
|
||||||
|
if (ran) return
|
||||||
|
ran = true
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCallOnce(cb: Cb): () => void
|
||||||
|
export function useCallOnce(cb?: undefined): (cb: Cb) => void
|
||||||
|
export function useCallOnce(cb?: Cb) {
|
||||||
|
const ran = useRef(false)
|
||||||
|
return useCallback(
|
||||||
|
(icb: Cb) => {
|
||||||
|
if (ran.current) return
|
||||||
|
ran.current = true
|
||||||
|
if (icb) icb()
|
||||||
|
else if (cb) cb()
|
||||||
|
},
|
||||||
|
[cb],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export type Gate =
|
|
||||||
// Keep this alphabetic please.
|
|
||||||
| 'debug_show_feedcontext'
|
|
||||||
| 'is_bsky_team_member' // special, do not remove
|
|
||||||
| 'disable_onboarding_find_contacts'
|
|
||||||
| 'disable_settings_find_contacts'
|
|
||||||
| 'disable_live_now_beta'
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user