Handle pressing all go.bsky.app links in-app w/ resolution (#4680)

zio/stable
Hailey 2024-06-27 19:35:20 -07:00 committed by GitHub
parent 030c8e268e
commit 91c4aa7c2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 186 additions and 17 deletions

View File

@ -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"

View File

@ -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)
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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',
})

View File

@ -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>

View File

@ -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,

View File

@ -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,
})
}

View File

@ -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) {