Handle pressing all go.bsky.app links in-app w/ resolution (#4680)
This commit is contained in:
parent
030c8e268e
commit
91c4aa7c2d
9 changed files with 186 additions and 17 deletions
|
@ -43,7 +43,10 @@ import HashtagScreen from '#/screens/Hashtag'
|
||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
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 {Wizard} from '#/screens/StarterPack/Wizard'
|
||||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||||
|
@ -322,7 +325,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="StarterPack"
|
name="StarterPack"
|
||||||
getComponent={() => StarterPackScreen}
|
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
|
<Stack.Screen
|
||||||
name="StarterPackWizard"
|
name="StarterPackWizard"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {startUriToStarterPackUri} from 'lib/strings/starter-pack'
|
|
||||||
|
|
||||||
export async function resolveShortLink(shortLink: string) {
|
export async function resolveShortLink(shortLink: string) {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
@ -8,15 +7,20 @@ export async function resolveShortLink(shortLink: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(shortLink, {
|
const res = await fetch(shortLink, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
|
logger.error('Failed to resolve short link', {status: res.status})
|
||||||
return shortLink
|
return shortLink
|
||||||
}
|
}
|
||||||
return startUriToStarterPackUri(res.url)
|
const json = (await res.json()) as {url: string}
|
||||||
|
return json.url
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
logger.error('Failed to resolve short link', {safeMessage: e})
|
logger.error('Failed to resolve short link', {safeMessage: e})
|
||||||
return null
|
return shortLink
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(to)
|
clearTimeout(to)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ export type CommonNavigatorParams = {
|
||||||
Feeds: undefined
|
Feeds: undefined
|
||||||
Start: {name: string; rkey: string}
|
Start: {name: string; rkey: string}
|
||||||
StarterPack: {name: string; rkey: string; new?: boolean}
|
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||||
|
StarterPackShort: {code: string}
|
||||||
StarterPackWizard: undefined
|
StarterPackWizard: undefined
|
||||||
StarterPackEdit: {
|
StarterPackEdit: {
|
||||||
rkey?: string
|
rkey?: string
|
||||||
|
@ -101,6 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||||
Messages: {animation?: 'push' | 'pop'}
|
Messages: {animation?: 'push' | 'pop'}
|
||||||
Start: {name: string; rkey: string}
|
Start: {name: string; rkey: string}
|
||||||
StarterPack: {name: string; rkey: string; new?: boolean}
|
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||||
|
StarterPackShort: {code: string}
|
||||||
StarterPackWizard: undefined
|
StarterPackWizard: undefined
|
||||||
StarterPackEdit: {
|
StarterPackEdit: {
|
||||||
rkey?: string
|
rkey?: string
|
||||||
|
|
|
@ -167,6 +167,9 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', 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
|
return url
|
||||||
}
|
}
|
||||||
|
@ -288,11 +291,21 @@ export function createBskyAppAbsoluteUrl(path: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isShortLink(url: string): boolean {
|
export function isShortLink(url: string): boolean {
|
||||||
|
return url.startsWith('https://go.bsky.app/')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shortLinkToHref(url: string): string {
|
||||||
try {
|
try {
|
||||||
const urlp = new URL(url)
|
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) {
|
} catch (e) {
|
||||||
logger.error('Failed to parse possible short link', {safeMessage: 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',
|
Start: '/start/:name/:rkey',
|
||||||
StarterPackEdit: '/starter-pack/edit/:rkey',
|
StarterPackEdit: '/starter-pack/edit/:rkey',
|
||||||
StarterPack: '/starter-pack/:name/:rkey',
|
StarterPack: '/starter-pack/:name/:rkey',
|
||||||
|
StarterPackShort: '/starter-pack-short/:code',
|
||||||
StarterPackWizard: '/starter-pack/create',
|
StarterPackWizard: '/starter-pack/create',
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
|
import {ChevronLeft_Stroke2_Corner0_Rounded} from '#/components/icons/Chevron'
|
||||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
import {ListMaybePlaceholder} from '#/components/Lists'
|
import {ListMaybePlaceholder} from '#/components/Lists'
|
||||||
import {Default as ProfileCard} from '#/components/ProfileCard'
|
import {Default as ProfileCard} from '#/components/ProfileCard'
|
||||||
|
@ -58,7 +59,11 @@ export function LandingScreen({
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const activeStarterPack = useActiveStarterPack()
|
const activeStarterPack = useActiveStarterPack()
|
||||||
|
|
||||||
const {data: starterPack, isError: isErrorStarterPack} = useStarterPackQuery({
|
const {
|
||||||
|
data: starterPack,
|
||||||
|
isError: isErrorStarterPack,
|
||||||
|
isFetching,
|
||||||
|
} = useStarterPackQuery({
|
||||||
uri: activeStarterPack?.uri,
|
uri: activeStarterPack?.uri,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -74,7 +79,7 @@ export function LandingScreen({
|
||||||
}
|
}
|
||||||
}, [isErrorStarterPack, setScreenState, isValid, starterPack])
|
}, [isErrorStarterPack, setScreenState, isValid, starterPack])
|
||||||
|
|
||||||
if (!starterPack || !isValid || !moderationOpts) {
|
if (isFetching || !starterPack || !isValid || !moderationOpts) {
|
||||||
return <ListMaybePlaceholder isLoading={true} />
|
return <ListMaybePlaceholder isLoading={true} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,9 +117,6 @@ function LandingScreenLoaded({
|
||||||
const listItemsCount = starterPack.list?.listItemCount ?? 0
|
const listItemsCount = starterPack.list?.listItemCount ?? 0
|
||||||
|
|
||||||
const onContinue = () => {
|
const onContinue = () => {
|
||||||
setActiveStarterPack({
|
|
||||||
uri: starterPack.uri,
|
|
||||||
})
|
|
||||||
setScreenState(LoggedOutScreenState.S_CreateAccount)
|
setScreenState(LoggedOutScreenState.S_CreateAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,6 +168,31 @@ function LandingScreenLoaded({
|
||||||
paddingTop: 100,
|
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]}>
|
<View style={[a.flex_row, a.gap_md, a.pb_sm]}>
|
||||||
<Logo width={76} fill="white" />
|
<Logo width={76} fill="white" />
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -28,15 +28,20 @@ import {HITSLOP_20} from 'lib/constants'
|
||||||
import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
|
import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
|
||||||
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
|
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
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 {isWeb} from 'platform/detection'
|
||||||
import {updateProfileShadow} from 'state/cache/profile-shadow'
|
import {updateProfileShadow} from 'state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from 'state/preferences/moderation-opts'
|
import {useModerationOpts} from 'state/preferences/moderation-opts'
|
||||||
import {useListMembersQuery} from 'state/queries/list-members'
|
import {useListMembersQuery} from 'state/queries/list-members'
|
||||||
|
import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
|
||||||
import {useResolveDidQuery} from 'state/queries/resolve-uri'
|
import {useResolveDidQuery} from 'state/queries/resolve-uri'
|
||||||
import {useShortenLink} from 'state/queries/shorten-link'
|
import {useShortenLink} from 'state/queries/shorten-link'
|
||||||
import {useStarterPackQuery} from 'state/queries/starter-packs'
|
import {useStarterPackQuery} from 'state/queries/starter-packs'
|
||||||
import {useAgent, useSession} from 'state/session'
|
import {useAgent, useSession} from 'state/session'
|
||||||
|
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||||
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
||||||
|
@ -67,12 +72,77 @@ type StarterPackScreeProps = NativeStackScreenProps<
|
||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
'StarterPack'
|
'StarterPack'
|
||||||
>
|
>
|
||||||
|
type StarterPackScreenShortProps = NativeStackScreenProps<
|
||||||
|
CommonNavigatorParams,
|
||||||
|
'StarterPackShort'
|
||||||
|
>
|
||||||
|
|
||||||
export function StarterPackScreen({route}: StarterPackScreeProps) {
|
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 {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const {name, rkey} = route.params
|
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const {
|
const {
|
||||||
data: did,
|
data: did,
|
||||||
|
@ -113,16 +183,16 @@ export function StarterPackScreen({route}: StarterPackScreeProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StarterPackScreenInner
|
<StarterPackScreenLoaded
|
||||||
starterPack={starterPack}
|
starterPack={starterPack}
|
||||||
routeParams={route.params}
|
routeParams={routeParams}
|
||||||
listMembersQuery={listMembersQuery}
|
listMembersQuery={listMembersQuery}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StarterPackScreenInner({
|
function StarterPackScreenLoaded({
|
||||||
starterPack,
|
starterPack,
|
||||||
routeParams,
|
routeParams,
|
||||||
listMembersQuery,
|
listMembersQuery,
|
||||||
|
|
24
src/state/queries/resolve-short-link.ts
Normal file
24
src/state/queries/resolve-short-link.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const activeStarterPack = useActiveStarterPack()
|
const activeStarterPack = useActiveStarterPack()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
|
const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
|
||||||
|
|
||||||
const [state, setState] = React.useState<State>({
|
const [state, setState] = React.useState<State>({
|
||||||
showLoggedOut: shouldShowStarterPack,
|
showLoggedOut: shouldShowStarterPack,
|
||||||
requestedAccountSwitchTo: shouldShowStarterPack
|
requestedAccountSwitchTo: shouldShowStarterPack
|
||||||
|
@ -59,6 +60,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
: undefined,
|
: 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>(
|
const controls = React.useMemo<Controls>(
|
||||||
() => ({
|
() => ({
|
||||||
setShowLoggedOut(show) {
|
setShowLoggedOut(show) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue