From ff23ddb556be4b2a9c4029dce6f857df34fc0b6b Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Thu, 21 Nov 2024 19:20:46 +0000
Subject: Don't underline links on native hover (#6588)

---
 src/components/Link.tsx | 18 +++++++-----------
 1 file changed, 7 insertions(+), 11 deletions(-)

(limited to 'src/components/Link.tsx')

diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 054a543c1..a5203b252 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -274,11 +274,6 @@ export function InlineLinkText({
     onOut: onHoverOut,
   } = useInteractionState()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-  const {
-    state: pressed,
-    onIn: onPressIn,
-    onOut: onPressOut,
-  } = useInteractionState()
   const flattenedStyle = flatten(style) || {}
 
   return (
@@ -289,19 +284,20 @@ export function InlineLinkText({
       {...rest}
       style={[
         {color: t.palette.primary_500},
-        (hovered || focused || pressed) &&
+        (hovered || focused) &&
           !disableUnderline && {
-            ...web({outline: 0}),
-            textDecorationLine: 'underline',
-            textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
+            ...web({
+              outline: 0,
+              textDecorationLine: 'underline',
+              textDecorationColor:
+                flattenedStyle.color ?? t.palette.primary_500,
+            }),
           },
         flattenedStyle,
       ]}
       role="link"
       onPress={download ? undefined : onPress}
       onLongPress={onLongPress}
-      onPressIn={onPressIn}
-      onPressOut={onPressOut}
       onFocus={onFocus}
       onBlur={onBlur}
       onMouseEnter={onHoverIn}
-- 
cgit 1.4.1


From ac5b2cf31f2bb45f1bf8a180705249d3cce8017d Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Fri, 22 Nov 2024 23:28:45 +0000
Subject: 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>
---
 .../icons/chainLink3_stroke2_corner0_rounded.svg   |  1 +
 src/components/Link.tsx                            |  4 +-
 src/components/icons/ChainLink.tsx                 |  5 ++
 src/lib/hooks/useOpenLink.ts                       | 24 +++++++++-
 src/screens/Settings/ContentAndMediaSettings.tsx   | 56 +++++++++++++++++-----
 src/state/persisted/schema.ts                      |  2 +
 src/state/preferences/index.tsx                    |  5 +-
 src/state/preferences/opt-out-of-utm.tsx           | 42 ++++++++++++++++
 src/view/com/util/Link.tsx                         |  2 +-
 9 files changed, 125 insertions(+), 16 deletions(-)
 create mode 100644 assets/icons/chainLink3_stroke2_corner0_rounded.svg
 create mode 100644 src/components/icons/ChainLink.tsx
 create mode 100644 src/state/preferences/opt-out-of-utm.tsx

(limited to 'src/components/Link.tsx')

diff --git a/assets/icons/chainLink3_stroke2_corner0_rounded.svg b/assets/icons/chainLink3_stroke2_corner0_rounded.svg
new file mode 100644
index 000000000..c1626cc61
--- /dev/null
+++ b/assets/icons/chainLink3_stroke2_corner0_rounded.svg
@@ -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>
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index a5203b252..ef31ea0c5 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -223,7 +223,7 @@ export function Link({
       {...web({
         hrefAttrs: {
           target: download ? undefined : isExternal ? 'blank' : undefined,
-          rel: isExternal ? 'noopener noreferrer' : undefined,
+          rel: isExternal ? 'noopener' : undefined,
           download,
         },
         dataSet: {
@@ -307,7 +307,7 @@ export function InlineLinkText({
       {...web({
         hrefAttrs: {
           target: download ? undefined : isExternal ? 'blank' : undefined,
-          rel: isExternal ? 'noopener noreferrer' : undefined,
+          rel: isExternal ? 'noopener' : undefined,
           download,
         },
         dataSet: {
diff --git a/src/components/icons/ChainLink.tsx b/src/components/icons/ChainLink.tsx
new file mode 100644
index 000000000..ba0b417a9
--- /dev/null
+++ b/src/components/icons/ChainLink.tsx
@@ -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',
+})
diff --git a/src/lib/hooks/useOpenLink.ts b/src/lib/hooks/useOpenLink.ts
index 5b75695b8..727821670 100644
--- a/src/lib/hooks/useOpenLink.ts
+++ b/src/lib/hooks/useOpenLink.ts
@@ -4,12 +4,14 @@ import * as WebBrowser from 'expo-web-browser'
 
 import {
   createBskyAppAbsoluteUrl,
+  isBskyAppUrl,
   isBskyRSSUrl,
   isRelativeUrl,
 } from '#/lib/strings/url-helpers'
 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 +20,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 +29,9 @@ export function useOpenLink() {
       }
 
       if (isNative && !url.startsWith('mailto:')) {
+        if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) {
+          url = addUtmSource(url)
+        }
         if (override === undefined && enabled === undefined) {
           openModal({
             name: 'in-app-browser-consent',
@@ -47,8 +53,24 @@ export function useOpenLink() {
       }
       Linking.openURL(url)
     },
-    [enabled, openModal, t, sheetWrapper],
+    [enabled, openModal, t, sheetWrapper, optOutOfUtm],
   )
 
   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()
+}
diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx
index b3fb8c174..27448ba9a 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 (
     <Layout.Screen>
@@ -68,6 +77,19 @@ export function ContentAndMediaSettingsScreen({}: Props) {
             </SettingsList.ItemText>
           </SettingsList.LinkItem>
           <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 && (
             <Toggle.Item
               name="use_in_app_browser"
@@ -83,19 +105,31 @@ export function ContentAndMediaSettingsScreen({}: Props) {
               </SettingsList.Item>
             </Toggle.Item>
           )}
-          <Toggle.Item
-            name="disable_autoplay"
-            label={_(msg`Autoplay videos and GIFs`)}
-            value={!autoplayDisabledPref}
-            onChange={value => setAutoplayDisabledPref(!value)}>
+          {isNative && <SettingsList.Divider />}
+          {isNative && (
+            <Toggle.Item
+              name="allow_utm"
+              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.ItemIcon icon={PlayIcon} />
-              <SettingsList.ItemText>
-                <Trans>Autoplay videos and GIFs</Trans>
-              </SettingsList.ItemText>
-              <Toggle.Platform />
+              <Admonition type="info" style={[a.flex_1]}>
+                <Trans>
+                  Helps external sites estimate traffic from Bluesky.
+                </Trans>
+              </Admonition>
             </SettingsList.Item>
-          </Toggle.Item>
+          )}
         </SettingsList.Container>
       </Layout.Content>
     </Layout.Screen>
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 804017949..85a6bf8e2 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<typeof schema>
 
@@ -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 c7eaf2726..43a08926e 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<{}>) {
                   <AutoplayProvider>
                     <UsedStarterPacksProvider>
                       <SubtitlesProvider>
-                        <KawaiiProvider>{children}</KawaiiProvider>
+                        <OutOutOfUtmProvider>
+                          <KawaiiProvider>{children}</KawaiiProvider>
+                        </OutOutOfUtmProvider>
                       </SubtitlesProvider>
                     </UsedStarterPacksProvider>
                   </AutoplayProvider>
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 000000000..40144c8db
--- /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<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)
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 489fbc59c..f83258e45 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -256,7 +256,7 @@ export const TextLink = memo(function TextLink({
     if (isExternal) {
       return {
         target: '_blank',
-        // rel: 'noopener noreferrer',
+        // rel: 'noopener',
       }
     }
     return {}
-- 
cgit 1.4.1

