Handle pressing all go.bsky.app links in-app w/ resolution (#4680)
parent
030c8e268e
commit
91c4aa7c2d
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue