Pass referrer on native (with an opt out) (#6648)

* Pass referer on native

* Add ChainLink3

* Add an opt out for sending utm

* Remove noreferrer on links

We do have <meta name="referrer" content="origin-when-cross-origin"> in HTML, should be sufficient.

* Narrow down the condition slightly

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
dan
2024-11-22 23:28:45 +00:00
committed by GitHub
parent fee2f5daa2
commit ac5b2cf31f
9 changed files with 125 additions and 16 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 610 B

+2 -2
View File
@@ -223,7 +223,7 @@ export function Link({
{...web({ {...web({
hrefAttrs: { hrefAttrs: {
target: download ? undefined : isExternal ? 'blank' : undefined, target: download ? undefined : isExternal ? 'blank' : undefined,
rel: isExternal ? 'noopener noreferrer' : undefined, rel: isExternal ? 'noopener' : undefined,
download, download,
}, },
dataSet: { dataSet: {
@@ -307,7 +307,7 @@ export function InlineLinkText({
{...web({ {...web({
hrefAttrs: { hrefAttrs: {
target: download ? undefined : isExternal ? 'blank' : undefined, target: download ? undefined : isExternal ? 'blank' : undefined,
rel: isExternal ? 'noopener noreferrer' : undefined, rel: isExternal ? 'noopener' : undefined,
download, download,
}, },
dataSet: { dataSet: {
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
})
+23 -1
View File
@@ -4,12 +4,14 @@ import * as WebBrowser from 'expo-web-browser'
import { import {
createBskyAppAbsoluteUrl, createBskyAppAbsoluteUrl,
isBskyAppUrl,
isBskyRSSUrl, isBskyRSSUrl,
isRelativeUrl, isRelativeUrl,
} from '#/lib/strings/url-helpers' } from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useInAppBrowser} from '#/state/preferences/in-app-browser' import {useInAppBrowser} from '#/state/preferences/in-app-browser'
import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
@@ -18,6 +20,7 @@ export function useOpenLink() {
const enabled = useInAppBrowser() const enabled = useInAppBrowser()
const t = useTheme() const t = useTheme()
const sheetWrapper = useSheetWrapper() const sheetWrapper = useSheetWrapper()
const optOutOfUtm = useOptOutOfUtm()
const openLink = useCallback( const openLink = useCallback(
async (url: string, override?: boolean) => { async (url: string, override?: boolean) => {
@@ -26,6 +29,9 @@ export function useOpenLink() {
} }
if (isNative && !url.startsWith('mailto:')) { if (isNative && !url.startsWith('mailto:')) {
if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) {
url = addUtmSource(url)
}
if (override === undefined && enabled === undefined) { if (override === undefined && enabled === undefined) {
openModal({ openModal({
name: 'in-app-browser-consent', name: 'in-app-browser-consent',
@@ -47,8 +53,24 @@ export function useOpenLink() {
} }
Linking.openURL(url) Linking.openURL(url)
}, },
[enabled, openModal, t, sheetWrapper], [enabled, openModal, t, sheetWrapper, optOutOfUtm],
) )
return openLink return openLink
} }
function addUtmSource(url: string): string {
let parsedUrl
try {
parsedUrl = new URL(url)
} catch (e) {
return url
}
if (!parsedUrl.searchParams.has('utm_source')) {
parsedUrl.searchParams.set('utm_source', 'bluesky')
if (!parsedUrl.searchParams.has('utm_medium')) {
parsedUrl.searchParams.set('utm_medium', 'social')
}
}
return parsedUrl.toString()
}
@@ -9,9 +9,16 @@ import {
useInAppBrowser, useInAppBrowser,
useSetInAppBrowser, useSetInAppBrowser,
} from '#/state/preferences/in-app-browser' } 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 * 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 * as Toggle from '#/components/forms/Toggle'
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' 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 {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home' import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh' import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
@@ -29,6 +36,8 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const setAutoplayDisabledPref = useSetAutoplayDisabled() const setAutoplayDisabledPref = useSetAutoplayDisabled()
const inAppBrowserPref = useInAppBrowser() const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser()
const optOutOfUtm = useOptOutOfUtm()
const setOptOutOfUtm = useSetOptOutOfUtm()
return ( return (
<Layout.Screen> <Layout.Screen>
@@ -68,6 +77,19 @@ export function ContentAndMediaSettingsScreen({}: Props) {
</SettingsList.ItemText> </SettingsList.ItemText>
</SettingsList.LinkItem> </SettingsList.LinkItem>
<SettingsList.Divider /> <SettingsList.Divider />
<Toggle.Item
name="disable_autoplay"
label={_(msg`Autoplay videos and GIFs`)}
value={!autoplayDisabledPref}
onChange={value => setAutoplayDisabledPref(!value)}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={PlayIcon} />
<SettingsList.ItemText>
<Trans>Autoplay videos and GIFs</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
{isNative && ( {isNative && (
<Toggle.Item <Toggle.Item
name="use_in_app_browser" name="use_in_app_browser"
@@ -83,19 +105,31 @@ export function ContentAndMediaSettingsScreen({}: Props) {
</SettingsList.Item> </SettingsList.Item>
</Toggle.Item> </Toggle.Item>
)} )}
<Toggle.Item {isNative && <SettingsList.Divider />}
name="disable_autoplay" {isNative && (
label={_(msg`Autoplay videos and GIFs`)} <Toggle.Item
value={!autoplayDisabledPref} name="allow_utm"
onChange={value => setAutoplayDisabledPref(!value)}> label={_(msg`Specify Bluesky as a referer`)}
value={!(optOutOfUtm ?? false)}
onChange={value => setOptOutOfUtm(!value)}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={ChainLinkIcon} />
<SettingsList.ItemText>
<Trans>Send Bluesky referrer</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
)}
{isNative && (
<SettingsList.Item> <SettingsList.Item>
<SettingsList.ItemIcon icon={PlayIcon} /> <Admonition type="info" style={[a.flex_1]}>
<SettingsList.ItemText> <Trans>
<Trans>Autoplay videos and GIFs</Trans> Helps external sites estimate traffic from Bluesky.
</SettingsList.ItemText> </Trans>
<Toggle.Platform /> </Admonition>
</SettingsList.Item> </SettingsList.Item>
</Toggle.Item> )}
</SettingsList.Container> </SettingsList.Container>
</Layout.Content> </Layout.Content>
</Layout.Screen> </Layout.Screen>
+2
View File
@@ -124,6 +124,7 @@ const schema = z.object({
subtitlesEnabled: z.boolean().optional(), subtitlesEnabled: z.boolean().optional(),
/** @deprecated */ /** @deprecated */
mutedThreads: z.array(z.string()), mutedThreads: z.array(z.string()),
optOutOfUtm: z.boolean().optional(),
}) })
export type Schema = z.infer<typeof schema> export type Schema = z.infer<typeof schema>
@@ -169,6 +170,7 @@ export const defaults: Schema = {
kawaii: false, kawaii: false,
hasCheckedForStarterPack: false, hasCheckedForStarterPack: false,
subtitlesEnabled: true, subtitlesEnabled: true,
optOutOfUtm: false,
} }
export function tryParse(rawData: string): Schema | undefined { export function tryParse(rawData: string): Schema | undefined {
+4 -1
View File
@@ -9,6 +9,7 @@ import {Provider as InAppBrowserProvider} from './in-app-browser'
import {Provider as KawaiiProvider} from './kawaii' import {Provider as KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages' import {Provider as LanguagesProvider} from './languages'
import {Provider as LargeAltBadgeProvider} from './large-alt-badge' 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 SubtitlesProvider} from './subtitles'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs' import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
@@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
<AutoplayProvider> <AutoplayProvider>
<UsedStarterPacksProvider> <UsedStarterPacksProvider>
<SubtitlesProvider> <SubtitlesProvider>
<KawaiiProvider>{children}</KawaiiProvider> <OutOutOfUtmProvider>
<KawaiiProvider>{children}</KawaiiProvider>
</OutOutOfUtmProvider>
</SubtitlesProvider> </SubtitlesProvider>
</UsedStarterPacksProvider> </UsedStarterPacksProvider>
</AutoplayProvider> </AutoplayProvider>
+42
View File
@@ -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<StateContext>(
Boolean(persisted.defaults.optOutOfUtm),
)
const setContext = React.createContext<SetContext>((_: 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 (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
export const useOptOutOfUtm = () => React.useContext(stateContext)
export const useSetOptOutOfUtm = () => React.useContext(setContext)
+1 -1
View File
@@ -256,7 +256,7 @@ export const TextLink = memo(function TextLink({
if (isExternal) { if (isExternal) {
return { return {
target: '_blank', target: '_blank',
// rel: 'noopener noreferrer', // rel: 'noopener',
} }
} }
return {} return {}