about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-06-27 19:35:20 -0700
committerGitHub <noreply@github.com>2024-06-27 19:35:20 -0700
commit91c4aa7c2dc598dd5e2c828e44c0d2c94cf0967d (patch)
tree362f79f88bab8107053c1fe0201ddcb4d0d21ac5
parent030c8e268e161bebe360e3ad97b1c18bd8425ca8 (diff)
downloadvoidsky-91c4aa7c2dc598dd5e2c828e44c0d2c94cf0967d.tar.zst
Handle pressing all go.bsky.app links in-app w/ resolution (#4680)
-rw-r--r--src/Navigation.tsx12
-rw-r--r--src/lib/link-meta/resolve-short-link.ts10
-rw-r--r--src/lib/routes/types.ts2
-rw-r--r--src/lib/strings/url-helpers.ts17
-rw-r--r--src/routes.ts1
-rw-r--r--src/screens/StarterPack/StarterPackLandingScreen.tsx37
-rw-r--r--src/screens/StarterPack/StarterPackScreen.tsx80
-rw-r--r--src/state/queries/resolve-short-link.ts24
-rw-r--r--src/state/shell/logged-out.tsx20
9 files changed, 186 insertions, 17 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 5cb4f4105..4ecf3fff8 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -43,7 +43,10 @@ import HashtagScreen from '#/screens/Hashtag'
 import {ModerationScreen} from '#/screens/Moderation'
 import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
-import {StarterPackScreen} from '#/screens/StarterPack/StarterPackScreen'
+import {
+  StarterPackScreen,
+  StarterPackScreenShort,
+} from '#/screens/StarterPack/StarterPackScreen'
 import {Wizard} from '#/screens/StarterPack/Wizard'
 import {init as initAnalytics} from './lib/analytics/analytics'
 import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
@@ -322,7 +325,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
       <Stack.Screen
         name="StarterPack"
         getComponent={() => StarterPackScreen}
-        options={{title: title(msg`Starter Pack`), requireAuth: true}}
+        options={{title: title(msg`Starter Pack`)}}
+      />
+      <Stack.Screen
+        name="StarterPackShort"
+        getComponent={() => StarterPackScreenShort}
+        options={{title: title(msg`Starter Pack`)}}
       />
       <Stack.Screen
         name="StarterPackWizard"
diff --git a/src/lib/link-meta/resolve-short-link.ts b/src/lib/link-meta/resolve-short-link.ts
index 3a3e2ab46..67d8a5860 100644
--- a/src/lib/link-meta/resolve-short-link.ts
+++ b/src/lib/link-meta/resolve-short-link.ts
@@ -1,5 +1,4 @@
 import {logger} from '#/logger'
-import {startUriToStarterPackUri} from 'lib/strings/starter-pack'
 
 export async function resolveShortLink(shortLink: string) {
   const controller = new AbortController()
@@ -8,15 +7,20 @@ export async function resolveShortLink(shortLink: string) {
   try {
     const res = await fetch(shortLink, {
       method: 'GET',
+      headers: {
+        Accept: 'application/json',
+      },
       signal: controller.signal,
     })
     if (res.status !== 200) {
+      logger.error('Failed to resolve short link', {status: res.status})
       return shortLink
     }
-    return startUriToStarterPackUri(res.url)
+    const json = (await res.json()) as {url: string}
+    return json.url
   } catch (e: unknown) {
     logger.error('Failed to resolve short link', {safeMessage: e})
-    return null
+    return shortLink
   } finally {
     clearTimeout(to)
   }
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 8a173b675..9d102f248 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -44,6 +44,7 @@ export type CommonNavigatorParams = {
   Feeds: undefined
   Start: {name: string; rkey: string}
   StarterPack: {name: string; rkey: string; new?: boolean}
+  StarterPackShort: {code: string}
   StarterPackWizard: undefined
   StarterPackEdit: {
     rkey?: string
@@ -101,6 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
   Messages: {animation?: 'push' | 'pop'}
   Start: {name: string; rkey: string}
   StarterPack: {name: string; rkey: string; new?: boolean}
+  StarterPackShort: {code: string}
   StarterPackWizard: undefined
   StarterPackEdit: {
     rkey?: string
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index b88b77f73..948279fce 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -167,6 +167,9 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
     } catch (e) {
       console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
     }
+  } else if (isShortLink(url)) {
+    // We only want to do this on native, web handles the 301 for us
+    return shortLinkToHref(url)
   }
   return url
 }
@@ -288,11 +291,21 @@ export function createBskyAppAbsoluteUrl(path: string): string {
 }
 
 export function isShortLink(url: string): boolean {
+  return url.startsWith('https://go.bsky.app/')
+}
+
+export function shortLinkToHref(url: string): string {
   try {
     const urlp = new URL(url)
-    return urlp.host === 'go.bsky.app'
+
+    // For now we only support starter packs, but in the future we should add additional paths to this check
+    const parts = urlp.pathname.split('/').filter(Boolean)
+    if (parts.length === 1) {
+      return `/starter-pack-short/${parts[0]}`
+    }
+    return url
   } catch (e) {
     logger.error('Failed to parse possible short link', {safeMessage: e})
-    return false
+    return url
   }
 }
diff --git a/src/routes.ts b/src/routes.ts
index f241d37a0..a76d8c4ce 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -44,5 +44,6 @@ export const router = new Router({
   Start: '/start/:name/:rkey',
   StarterPackEdit: '/starter-pack/edit/:rkey',
   StarterPack: '/starter-pack/:name/:rkey',
+  StarterPackShort: '/starter-pack-short/:code',
   StarterPackWizard: '/starter-pack/create',
 })
diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx
index df13885e8..12420333d 100644
--- a/src/screens/StarterPack/StarterPackLandingScreen.tsx
+++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx
@@ -31,6 +31,7 @@ import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import {useDialogControl} from '#/components/Dialog'
 import * as FeedCard from '#/components/FeedCard'
+import {ChevronLeft_Stroke2_Corner0_Rounded} from '#/components/icons/Chevron'
 import {LinearGradientBackground} from '#/components/LinearGradientBackground'
 import {ListMaybePlaceholder} from '#/components/Lists'
 import {Default as ProfileCard} from '#/components/ProfileCard'
@@ -58,7 +59,11 @@ export function LandingScreen({
   const moderationOpts = useModerationOpts()
   const activeStarterPack = useActiveStarterPack()
 
-  const {data: starterPack, isError: isErrorStarterPack} = useStarterPackQuery({
+  const {
+    data: starterPack,
+    isError: isErrorStarterPack,
+    isFetching,
+  } = useStarterPackQuery({
     uri: activeStarterPack?.uri,
   })
 
@@ -74,7 +79,7 @@ export function LandingScreen({
     }
   }, [isErrorStarterPack, setScreenState, isValid, starterPack])
 
-  if (!starterPack || !isValid || !moderationOpts) {
+  if (isFetching || !starterPack || !isValid || !moderationOpts) {
     return <ListMaybePlaceholder isLoading={true} />
   }
 
@@ -112,9 +117,6 @@ function LandingScreenLoaded({
   const listItemsCount = starterPack.list?.listItemCount ?? 0
 
   const onContinue = () => {
-    setActiveStarterPack({
-      uri: starterPack.uri,
-    })
     setScreenState(LoggedOutScreenState.S_CreateAccount)
   }
 
@@ -166,6 +168,31 @@ function LandingScreenLoaded({
               paddingTop: 100,
             },
           ]}>
+          <Pressable
+            style={[
+              a.absolute,
+              a.rounded_full,
+              a.align_center,
+              a.justify_center,
+              {
+                top: 10,
+                left: 10,
+                height: 35,
+                width: 35,
+                backgroundColor: 'rgba(0, 0, 0, 0.5)',
+              },
+            ]}
+            onPress={() => {
+              setActiveStarterPack(undefined)
+            }}
+            accessibilityLabel={_(msg`Back`)}
+            accessibilityHint={_(msg`Go back to previous screen`)}>
+            <ChevronLeft_Stroke2_Corner0_Rounded
+              width={20}
+              height={20}
+              fill="white"
+            />
+          </Pressable>
           <View style={[a.flex_row, a.gap_md, a.pb_sm]}>
             <Logo width={76} fill="white" />
           </View>
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index aa0e75a23..679b3f2cb 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -28,15 +28,20 @@ import {HITSLOP_20} from 'lib/constants'
 import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
 import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
 import {logEvent} from 'lib/statsig/statsig'
-import {getStarterPackOgCard} from 'lib/strings/starter-pack'
+import {
+  createStarterPackUri,
+  getStarterPackOgCard,
+} from 'lib/strings/starter-pack'
 import {isWeb} from 'platform/detection'
 import {updateProfileShadow} from 'state/cache/profile-shadow'
 import {useModerationOpts} from 'state/preferences/moderation-opts'
 import {useListMembersQuery} from 'state/queries/list-members'
+import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
 import {useResolveDidQuery} from 'state/queries/resolve-uri'
 import {useShortenLink} from 'state/queries/shorten-link'
 import {useStarterPackQuery} from 'state/queries/starter-packs'
 import {useAgent, useSession} from 'state/session'
+import {useSetActiveStarterPack} from 'state/shell/starter-pack'
 import * as Toast from '#/view/com/util/Toast'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
@@ -67,12 +72,77 @@ type StarterPackScreeProps = NativeStackScreenProps<
   CommonNavigatorParams,
   'StarterPack'
 >
+type StarterPackScreenShortProps = NativeStackScreenProps<
+  CommonNavigatorParams,
+  'StarterPackShort'
+>
 
 export function StarterPackScreen({route}: StarterPackScreeProps) {
+  return <StarterPackAuthCheck routeParams={route.params} />
+}
+
+export function StarterPackScreenShort({route}: StarterPackScreenShortProps) {
+  const {_} = useLingui()
+  const {
+    data: resolvedStarterPack,
+    isLoading,
+    isError,
+  } = useResolvedStarterPackShortLink({
+    code: route.params.code,
+  })
+
+  if (isLoading || isError || !resolvedStarterPack) {
+    return (
+      <ListMaybePlaceholder
+        isLoading={isLoading}
+        isError={isError}
+        errorMessage={_(msg`That starter pack could not be found.`)}
+        emptyMessage={_(msg`That starter pack could not be found.`)}
+      />
+    )
+  }
+  return <StarterPackAuthCheck routeParams={resolvedStarterPack} />
+}
+
+export function StarterPackAuthCheck({
+  routeParams,
+}: {
+  routeParams: StarterPackScreeProps['route']['params']
+}) {
+  const navigation = useNavigation<NavigationProp>()
+  const setActiveStarterPack = useSetActiveStarterPack()
+  const {currentAccount} = useSession()
+
+  React.useEffect(() => {
+    if (currentAccount) return
+
+    const uri = createStarterPackUri({
+      did: routeParams.name,
+      rkey: routeParams.rkey,
+    })
+
+    if (!uri) return
+    setActiveStarterPack({
+      uri,
+    })
+
+    navigation.goBack()
+  }, [routeParams, currentAccount, navigation, setActiveStarterPack])
+
+  if (!currentAccount) return null
+
+  return <StarterPackScreenInner routeParams={routeParams} />
+}
+
+export function StarterPackScreenInner({
+  routeParams,
+}: {
+  routeParams: StarterPackScreeProps['route']['params']
+}) {
+  const {name, rkey} = routeParams
   const {_} = useLingui()
   const {currentAccount} = useSession()
 
-  const {name, rkey} = route.params
   const moderationOpts = useModerationOpts()
   const {
     data: did,
@@ -113,16 +183,16 @@ export function StarterPackScreen({route}: StarterPackScreeProps) {
   }
 
   return (
-    <StarterPackScreenInner
+    <StarterPackScreenLoaded
       starterPack={starterPack}
-      routeParams={route.params}
+      routeParams={routeParams}
       listMembersQuery={listMembersQuery}
       moderationOpts={moderationOpts}
     />
   )
 }
 
-function StarterPackScreenInner({
+function StarterPackScreenLoaded({
   starterPack,
   routeParams,
   listMembersQuery,
diff --git a/src/state/queries/resolve-short-link.ts b/src/state/queries/resolve-short-link.ts
new file mode 100644
index 000000000..a10bc12c1
--- /dev/null
+++ b/src/state/queries/resolve-short-link.ts
@@ -0,0 +1,24 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {resolveShortLink} from 'lib/link-meta/resolve-short-link'
+import {parseStarterPackUri} from 'lib/strings/starter-pack'
+import {STALE} from 'state/queries/index'
+
+const ROOT_URI = 'https://go.bsky.app/'
+
+const RQKEY_ROOT = 'resolved-short-link'
+export const RQKEY = (code: string) => [RQKEY_ROOT, code]
+
+export function useResolvedStarterPackShortLink({code}: {code: string}) {
+  return useQuery({
+    queryKey: RQKEY(code),
+    queryFn: async () => {
+      const uri = `${ROOT_URI}${code}`
+      const res = await resolveShortLink(uri)
+      return parseStarterPackUri(res)
+    },
+    retry: 1,
+    enabled: Boolean(code),
+    staleTime: STALE.HOURS.ONE,
+  })
+}
diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx
index dc78d03d5..2c577fdd2 100644
--- a/src/state/shell/logged-out.tsx
+++ b/src/state/shell/logged-out.tsx
@@ -50,6 +50,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const activeStarterPack = useActiveStarterPack()
   const {hasSession} = useSession()
   const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
+
   const [state, setState] = React.useState<State>({
     showLoggedOut: shouldShowStarterPack,
     requestedAccountSwitchTo: shouldShowStarterPack
@@ -59,6 +60,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       : undefined,
   })
 
+  const [prevActiveStarterPack, setPrevActiveStarterPack] =
+    React.useState(activeStarterPack)
+  if (activeStarterPack?.uri !== prevActiveStarterPack?.uri) {
+    setPrevActiveStarterPack(activeStarterPack)
+    if (activeStarterPack) {
+      setState(s => ({
+        ...s,
+        showLoggedOut: true,
+        requestedAccountSwitchTo: 'starterpack',
+      }))
+    } else {
+      setState(s => ({
+        ...s,
+        showLoggedOut: false,
+        requestedAccountSwitchTo: undefined,
+      }))
+    }
+  }
+
   const controls = React.useMemo<Controls>(
     () => ({
       setShowLoggedOut(show) {