diff --git a/src/lib/hooks/useOpenLink.ts b/src/lib/hooks/useOpenLink.ts
index 468992ac5e..035212f114 100644
--- a/src/lib/hooks/useOpenLink.ts
+++ b/src/lib/hooks/useOpenLink.ts
@@ -10,6 +10,7 @@ import {
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useInAppBrowser} from '#/state/preferences/in-app-browser'
+import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm'
import {useTheme} from '#/alf'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
@@ -18,6 +19,7 @@ export function useOpenLink() {
const enabled = useInAppBrowser()
const t = useTheme()
const sheetWrapper = useSheetWrapper()
+ const optOutOfUtm = useOptOutOfUtm()
const openLink = useCallback(
async (url: string, override?: boolean) => {
@@ -26,6 +28,9 @@ export function useOpenLink() {
}
if (isNative && !url.startsWith('mailto:')) {
+ if (!optOutOfUtm) {
+ url = addUtmSource(url)
+ }
if (override === undefined && enabled === undefined) {
openModal({
name: 'in-app-browser-consent',
@@ -33,7 +38,6 @@ export function useOpenLink() {
})
return
} else if (override ?? enabled) {
- url = addUtmSource(url)
await sheetWrapper(
WebBrowser.openBrowserAsync(url, {
presentationStyle:
@@ -48,7 +52,7 @@ export function useOpenLink() {
}
Linking.openURL(url)
},
- [enabled, openModal, t, sheetWrapper],
+ [enabled, openModal, t, sheetWrapper, optOutOfUtm],
)
return openLink
diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx
index b3fb8c174f..27448ba9af 100644
--- a/src/screens/Settings/ContentAndMediaSettings.tsx
+++ b/src/screens/Settings/ContentAndMediaSettings.tsx
@@ -9,9 +9,16 @@ import {
useInAppBrowser,
useSetInAppBrowser,
} from '#/state/preferences/in-app-browser'
+import {
+ useOptOutOfUtm,
+ useSetOptOutOfUtm,
+} from '#/state/preferences/opt-out-of-utm'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
+import {atoms as a} from '#/alf'
+import {Admonition} from '#/components/Admonition'
import * as Toggle from '#/components/forms/Toggle'
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
+import {ChainLink3_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
@@ -29,6 +36,8 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const setAutoplayDisabledPref = useSetAutoplayDisabled()
const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser()
+ const optOutOfUtm = useOptOutOfUtm()
+ const setOptOutOfUtm = useSetOptOutOfUtm()
return (
@@ -68,6 +77,19 @@ export function ContentAndMediaSettingsScreen({}: Props) {
+ setAutoplayDisabledPref(!value)}>
+
+
+
+ Autoplay videos and GIFs
+
+
+
+
{isNative && (
)}
- setAutoplayDisabledPref(!value)}>
+ {isNative && }
+ {isNative && (
+ setOptOutOfUtm(!value)}>
+
+
+
+ Send Bluesky referrer
+
+
+
+
+ )}
+ {isNative && (
-
-
- Autoplay videos and GIFs
-
-
+
+
+ Helps external sites estimate traffic from Bluesky.
+
+
-
+ )}
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 8040179496..85a6bf8e20 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -124,6 +124,7 @@ const schema = z.object({
subtitlesEnabled: z.boolean().optional(),
/** @deprecated */
mutedThreads: z.array(z.string()),
+ optOutOfUtm: z.boolean().optional(),
})
export type Schema = z.infer
@@ -169,6 +170,7 @@ export const defaults: Schema = {
kawaii: false,
hasCheckedForStarterPack: false,
subtitlesEnabled: true,
+ optOutOfUtm: false,
}
export function tryParse(rawData: string): Schema | undefined {
diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx
index c7eaf27261..43a08926e8 100644
--- a/src/state/preferences/index.tsx
+++ b/src/state/preferences/index.tsx
@@ -9,6 +9,7 @@ import {Provider as InAppBrowserProvider} from './in-app-browser'
import {Provider as KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages'
import {Provider as LargeAltBadgeProvider} from './large-alt-badge'
+import {Provider as OutOutOfUtmProvider} from './opt-out-of-utm'
import {Provider as SubtitlesProvider} from './subtitles'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
@@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
- {children}
+
+ {children}
+
diff --git a/src/state/preferences/opt-out-of-utm.tsx b/src/state/preferences/opt-out-of-utm.tsx
new file mode 100644
index 0000000000..40144c8db5
--- /dev/null
+++ b/src/state/preferences/opt-out-of-utm.tsx
@@ -0,0 +1,42 @@
+import React from 'react'
+
+import * as persisted from '#/state/persisted'
+
+type StateContext = boolean
+type SetContext = (v: boolean) => void
+
+const stateContext = React.createContext(
+ Boolean(persisted.defaults.optOutOfUtm),
+)
+const setContext = React.createContext((_: boolean) => {})
+
+export function Provider({children}: {children: React.ReactNode}) {
+ const [state, setState] = React.useState(
+ Boolean(persisted.get('optOutOfUtm')),
+ )
+
+ const setStateWrapped = React.useCallback(
+ (optOutOfUtm: persisted.Schema['optOutOfUtm']) => {
+ setState(Boolean(optOutOfUtm))
+ persisted.write('optOutOfUtm', optOutOfUtm)
+ },
+ [setState],
+ )
+
+ React.useEffect(() => {
+ return persisted.onUpdate('optOutOfUtm', nextOptOutOfUtm => {
+ setState(Boolean(nextOptOutOfUtm))
+ })
+ }, [setStateWrapped])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export const useOptOutOfUtm = () => React.useContext(stateContext)
+export const useSetOptOutOfUtm = () => React.useContext(setContext)