my fork of the bluesky client

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>

authored by danabra.mov

Eric Bailey and committed by
GitHub
ac5b2cf3 fee2f5da

+125 -16
+1
assets/icons/chainLink3_stroke2_corner0_rounded.svg
··· 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>
+2 -2
src/components/Link.tsx
··· 223 223 {...web({ 224 224 hrefAttrs: { 225 225 target: download ? undefined : isExternal ? 'blank' : undefined, 226 - rel: isExternal ? 'noopener noreferrer' : undefined, 226 + rel: isExternal ? 'noopener' : undefined, 227 227 download, 228 228 }, 229 229 dataSet: { ··· 307 307 {...web({ 308 308 hrefAttrs: { 309 309 target: download ? undefined : isExternal ? 'blank' : undefined, 310 - rel: isExternal ? 'noopener noreferrer' : undefined, 310 + rel: isExternal ? 'noopener' : undefined, 311 311 download, 312 312 }, 313 313 dataSet: {
+5
src/components/icons/ChainLink.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
+23 -1
src/lib/hooks/useOpenLink.ts
··· 4 4 5 5 import { 6 6 createBskyAppAbsoluteUrl, 7 + isBskyAppUrl, 7 8 isBskyRSSUrl, 8 9 isRelativeUrl, 9 10 } from '#/lib/strings/url-helpers' 10 11 import {isNative} from '#/platform/detection' 11 12 import {useModalControls} from '#/state/modals' 12 13 import {useInAppBrowser} from '#/state/preferences/in-app-browser' 14 + import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm' 13 15 import {useTheme} from '#/alf' 14 16 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 15 17 ··· 18 20 const enabled = useInAppBrowser() 19 21 const t = useTheme() 20 22 const sheetWrapper = useSheetWrapper() 23 + const optOutOfUtm = useOptOutOfUtm() 21 24 22 25 const openLink = useCallback( 23 26 async (url: string, override?: boolean) => { ··· 26 29 } 27 30 28 31 if (isNative && !url.startsWith('mailto:')) { 32 + if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) { 33 + url = addUtmSource(url) 34 + } 29 35 if (override === undefined && enabled === undefined) { 30 36 openModal({ 31 37 name: 'in-app-browser-consent', ··· 47 53 } 48 54 Linking.openURL(url) 49 55 }, 50 - [enabled, openModal, t, sheetWrapper], 56 + [enabled, openModal, t, sheetWrapper, optOutOfUtm], 51 57 ) 52 58 53 59 return openLink 54 60 } 61 + 62 + function addUtmSource(url: string): string { 63 + let parsedUrl 64 + try { 65 + parsedUrl = new URL(url) 66 + } catch (e) { 67 + return url 68 + } 69 + if (!parsedUrl.searchParams.has('utm_source')) { 70 + parsedUrl.searchParams.set('utm_source', 'bluesky') 71 + if (!parsedUrl.searchParams.has('utm_medium')) { 72 + parsedUrl.searchParams.set('utm_medium', 'social') 73 + } 74 + } 75 + return parsedUrl.toString() 76 + }
+45 -11
src/screens/Settings/ContentAndMediaSettings.tsx
··· 9 9 useInAppBrowser, 10 10 useSetInAppBrowser, 11 11 } from '#/state/preferences/in-app-browser' 12 + import { 13 + useOptOutOfUtm, 14 + useSetOptOutOfUtm, 15 + } from '#/state/preferences/opt-out-of-utm' 12 16 import * as SettingsList from '#/screens/Settings/components/SettingsList' 17 + import {atoms as a} from '#/alf' 18 + import {Admonition} from '#/components/Admonition' 13 19 import * as Toggle from '#/components/forms/Toggle' 14 20 import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' 21 + import {ChainLink3_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 15 22 import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag' 16 23 import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home' 17 24 import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh' ··· 29 36 const setAutoplayDisabledPref = useSetAutoplayDisabled() 30 37 const inAppBrowserPref = useInAppBrowser() 31 38 const setUseInAppBrowser = useSetInAppBrowser() 39 + const optOutOfUtm = useOptOutOfUtm() 40 + const setOptOutOfUtm = useSetOptOutOfUtm() 32 41 33 42 return ( 34 43 <Layout.Screen> ··· 68 77 </SettingsList.ItemText> 69 78 </SettingsList.LinkItem> 70 79 <SettingsList.Divider /> 80 + <Toggle.Item 81 + name="disable_autoplay" 82 + label={_(msg`Autoplay videos and GIFs`)} 83 + value={!autoplayDisabledPref} 84 + onChange={value => setAutoplayDisabledPref(!value)}> 85 + <SettingsList.Item> 86 + <SettingsList.ItemIcon icon={PlayIcon} /> 87 + <SettingsList.ItemText> 88 + <Trans>Autoplay videos and GIFs</Trans> 89 + </SettingsList.ItemText> 90 + <Toggle.Platform /> 91 + </SettingsList.Item> 92 + </Toggle.Item> 71 93 {isNative && ( 72 94 <Toggle.Item 73 95 name="use_in_app_browser" ··· 83 105 </SettingsList.Item> 84 106 </Toggle.Item> 85 107 )} 86 - <Toggle.Item 87 - name="disable_autoplay" 88 - label={_(msg`Autoplay videos and GIFs`)} 89 - value={!autoplayDisabledPref} 90 - onChange={value => setAutoplayDisabledPref(!value)}> 108 + {isNative && <SettingsList.Divider />} 109 + {isNative && ( 110 + <Toggle.Item 111 + name="allow_utm" 112 + label={_(msg`Specify Bluesky as a referer`)} 113 + value={!(optOutOfUtm ?? false)} 114 + onChange={value => setOptOutOfUtm(!value)}> 115 + <SettingsList.Item> 116 + <SettingsList.ItemIcon icon={ChainLinkIcon} /> 117 + <SettingsList.ItemText> 118 + <Trans>Send Bluesky referrer</Trans> 119 + </SettingsList.ItemText> 120 + <Toggle.Platform /> 121 + </SettingsList.Item> 122 + </Toggle.Item> 123 + )} 124 + {isNative && ( 91 125 <SettingsList.Item> 92 - <SettingsList.ItemIcon icon={PlayIcon} /> 93 - <SettingsList.ItemText> 94 - <Trans>Autoplay videos and GIFs</Trans> 95 - </SettingsList.ItemText> 96 - <Toggle.Platform /> 126 + <Admonition type="info" style={[a.flex_1]}> 127 + <Trans> 128 + Helps external sites estimate traffic from Bluesky. 129 + </Trans> 130 + </Admonition> 97 131 </SettingsList.Item> 98 - </Toggle.Item> 132 + )} 99 133 </SettingsList.Container> 100 134 </Layout.Content> 101 135 </Layout.Screen>
+2
src/state/persisted/schema.ts
··· 124 124 subtitlesEnabled: z.boolean().optional(), 125 125 /** @deprecated */ 126 126 mutedThreads: z.array(z.string()), 127 + optOutOfUtm: z.boolean().optional(), 127 128 }) 128 129 export type Schema = z.infer<typeof schema> 129 130 ··· 169 170 kawaii: false, 170 171 hasCheckedForStarterPack: false, 171 172 subtitlesEnabled: true, 173 + optOutOfUtm: false, 172 174 } 173 175 174 176 export function tryParse(rawData: string): Schema | undefined {
+4 -1
src/state/preferences/index.tsx
··· 9 9 import {Provider as KawaiiProvider} from './kawaii' 10 10 import {Provider as LanguagesProvider} from './languages' 11 11 import {Provider as LargeAltBadgeProvider} from './large-alt-badge' 12 + import {Provider as OutOutOfUtmProvider} from './opt-out-of-utm' 12 13 import {Provider as SubtitlesProvider} from './subtitles' 13 14 import {Provider as UsedStarterPacksProvider} from './used-starter-packs' 14 15 ··· 39 40 <AutoplayProvider> 40 41 <UsedStarterPacksProvider> 41 42 <SubtitlesProvider> 42 - <KawaiiProvider>{children}</KawaiiProvider> 43 + <OutOutOfUtmProvider> 44 + <KawaiiProvider>{children}</KawaiiProvider> 45 + </OutOutOfUtmProvider> 43 46 </SubtitlesProvider> 44 47 </UsedStarterPacksProvider> 45 48 </AutoplayProvider>
+42
src/state/preferences/opt-out-of-utm.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = boolean 6 + type SetContext = (v: boolean) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + Boolean(persisted.defaults.optOutOfUtm), 10 + ) 11 + const setContext = React.createContext<SetContext>((_: boolean) => {}) 12 + 13 + export function Provider({children}: {children: React.ReactNode}) { 14 + const [state, setState] = React.useState( 15 + Boolean(persisted.get('optOutOfUtm')), 16 + ) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (optOutOfUtm: persisted.Schema['optOutOfUtm']) => { 20 + setState(Boolean(optOutOfUtm)) 21 + persisted.write('optOutOfUtm', optOutOfUtm) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('optOutOfUtm', nextOptOutOfUtm => { 28 + setState(Boolean(nextOptOutOfUtm)) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export const useOptOutOfUtm = () => React.useContext(stateContext) 42 + export const useSetOptOutOfUtm = () => React.useContext(setContext)
+1 -1
src/view/com/util/Link.tsx
··· 256 256 if (isExternal) { 257 257 return { 258 258 target: '_blank', 259 - // rel: 'noopener noreferrer', 259 + // rel: 'noopener', 260 260 } 261 261 } 262 262 return {}