diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 12 | ||||
-rw-r--r-- | src/lib/link-meta/resolve-short-link.ts | 10 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 2 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 17 | ||||
-rw-r--r-- | src/routes.ts | 1 | ||||
-rw-r--r-- | src/screens/StarterPack/StarterPackLandingScreen.tsx | 37 | ||||
-rw-r--r-- | src/screens/StarterPack/StarterPackScreen.tsx | 80 | ||||
-rw-r--r-- | src/state/queries/resolve-short-link.ts | 24 | ||||
-rw-r--r-- | src/state/shell/logged-out.tsx | 20 |
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) { |