An ATproto social media client -- with an independent Appview.

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

authored by hailey.at and committed by GitHub 91c4aa7c 030c8e26

+186 -17
+10 -2
src/Navigation.tsx
··· 43 43 import {ModerationScreen} from '#/screens/Moderation' 44 44 import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' 45 45 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' 46 - import {StarterPackScreen} from '#/screens/StarterPack/StarterPackScreen' 46 + import { 47 + StarterPackScreen, 48 + StarterPackScreenShort, 49 + } from '#/screens/StarterPack/StarterPackScreen' 47 50 import {Wizard} from '#/screens/StarterPack/Wizard' 48 51 import {init as initAnalytics} from './lib/analytics/analytics' 49 52 import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' ··· 322 325 <Stack.Screen 323 326 name="StarterPack" 324 327 getComponent={() => StarterPackScreen} 325 - options={{title: title(msg`Starter Pack`), requireAuth: true}} 328 + options={{title: title(msg`Starter Pack`)}} 329 + /> 330 + <Stack.Screen 331 + name="StarterPackShort" 332 + getComponent={() => StarterPackScreenShort} 333 + options={{title: title(msg`Starter Pack`)}} 326 334 /> 327 335 <Stack.Screen 328 336 name="StarterPackWizard"
+2
src/lib/routes/types.ts
··· 44 44 Feeds: undefined 45 45 Start: {name: string; rkey: string} 46 46 StarterPack: {name: string; rkey: string; new?: boolean} 47 + StarterPackShort: {code: string} 47 48 StarterPackWizard: undefined 48 49 StarterPackEdit: { 49 50 rkey?: string ··· 101 102 Messages: {animation?: 'push' | 'pop'} 102 103 Start: {name: string; rkey: string} 103 104 StarterPack: {name: string; rkey: string; new?: boolean} 105 + StarterPackShort: {code: string} 104 106 StarterPackWizard: undefined 105 107 StarterPackEdit: { 106 108 rkey?: string
+15 -2
src/lib/strings/url-helpers.ts
··· 167 167 } catch (e) { 168 168 console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e) 169 169 } 170 + } else if (isShortLink(url)) { 171 + // We only want to do this on native, web handles the 301 for us 172 + return shortLinkToHref(url) 170 173 } 171 174 return url 172 175 } ··· 288 291 } 289 292 290 293 export function isShortLink(url: string): boolean { 294 + return url.startsWith('https://go.bsky.app/') 295 + } 296 + 297 + export function shortLinkToHref(url: string): string { 291 298 try { 292 299 const urlp = new URL(url) 293 - return urlp.host === 'go.bsky.app' 300 + 301 + // For now we only support starter packs, but in the future we should add additional paths to this check 302 + const parts = urlp.pathname.split('/').filter(Boolean) 303 + if (parts.length === 1) { 304 + return `/starter-pack-short/${parts[0]}` 305 + } 306 + return url 294 307 } catch (e) { 295 308 logger.error('Failed to parse possible short link', {safeMessage: e}) 296 - return false 309 + return url 297 310 } 298 311 }
+1
src/routes.ts
··· 44 44 Start: '/start/:name/:rkey', 45 45 StarterPackEdit: '/starter-pack/edit/:rkey', 46 46 StarterPack: '/starter-pack/:name/:rkey', 47 + StarterPackShort: '/starter-pack-short/:code', 47 48 StarterPackWizard: '/starter-pack/create', 48 49 })
+32 -5
src/screens/StarterPack/StarterPackLandingScreen.tsx
··· 31 31 import {Button, ButtonText} from '#/components/Button' 32 32 import {useDialogControl} from '#/components/Dialog' 33 33 import * as FeedCard from '#/components/FeedCard' 34 + import {ChevronLeft_Stroke2_Corner0_Rounded} from '#/components/icons/Chevron' 34 35 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 35 36 import {ListMaybePlaceholder} from '#/components/Lists' 36 37 import {Default as ProfileCard} from '#/components/ProfileCard' ··· 58 59 const moderationOpts = useModerationOpts() 59 60 const activeStarterPack = useActiveStarterPack() 60 61 61 - const {data: starterPack, isError: isErrorStarterPack} = useStarterPackQuery({ 62 + const { 63 + data: starterPack, 64 + isError: isErrorStarterPack, 65 + isFetching, 66 + } = useStarterPackQuery({ 62 67 uri: activeStarterPack?.uri, 63 68 }) 64 69 ··· 74 79 } 75 80 }, [isErrorStarterPack, setScreenState, isValid, starterPack]) 76 81 77 - if (!starterPack || !isValid || !moderationOpts) { 82 + if (isFetching || !starterPack || !isValid || !moderationOpts) { 78 83 return <ListMaybePlaceholder isLoading={true} /> 79 84 } 80 85 ··· 112 117 const listItemsCount = starterPack.list?.listItemCount ?? 0 113 118 114 119 const onContinue = () => { 115 - setActiveStarterPack({ 116 - uri: starterPack.uri, 117 - }) 118 120 setScreenState(LoggedOutScreenState.S_CreateAccount) 119 121 } 120 122 ··· 166 168 paddingTop: 100, 167 169 }, 168 170 ]}> 171 + <Pressable 172 + style={[ 173 + a.absolute, 174 + a.rounded_full, 175 + a.align_center, 176 + a.justify_center, 177 + { 178 + top: 10, 179 + left: 10, 180 + height: 35, 181 + width: 35, 182 + backgroundColor: 'rgba(0, 0, 0, 0.5)', 183 + }, 184 + ]} 185 + onPress={() => { 186 + setActiveStarterPack(undefined) 187 + }} 188 + accessibilityLabel={_(msg`Back`)} 189 + accessibilityHint={_(msg`Go back to previous screen`)}> 190 + <ChevronLeft_Stroke2_Corner0_Rounded 191 + width={20} 192 + height={20} 193 + fill="white" 194 + /> 195 + </Pressable> 169 196 <View style={[a.flex_row, a.gap_md, a.pb_sm]}> 170 197 <Logo width={76} fill="white" /> 171 198 </View>
+75 -5
src/screens/StarterPack/StarterPackScreen.tsx
··· 28 28 import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links' 29 29 import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' 30 30 import {logEvent} from 'lib/statsig/statsig' 31 - import {getStarterPackOgCard} from 'lib/strings/starter-pack' 31 + import { 32 + createStarterPackUri, 33 + getStarterPackOgCard, 34 + } from 'lib/strings/starter-pack' 32 35 import {isWeb} from 'platform/detection' 33 36 import {updateProfileShadow} from 'state/cache/profile-shadow' 34 37 import {useModerationOpts} from 'state/preferences/moderation-opts' 35 38 import {useListMembersQuery} from 'state/queries/list-members' 39 + import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link' 36 40 import {useResolveDidQuery} from 'state/queries/resolve-uri' 37 41 import {useShortenLink} from 'state/queries/shorten-link' 38 42 import {useStarterPackQuery} from 'state/queries/starter-packs' 39 43 import {useAgent, useSession} from 'state/session' 44 + import {useSetActiveStarterPack} from 'state/shell/starter-pack' 40 45 import * as Toast from '#/view/com/util/Toast' 41 46 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' 42 47 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' ··· 67 72 CommonNavigatorParams, 68 73 'StarterPack' 69 74 > 75 + type StarterPackScreenShortProps = NativeStackScreenProps< 76 + CommonNavigatorParams, 77 + 'StarterPackShort' 78 + > 70 79 71 80 export function StarterPackScreen({route}: StarterPackScreeProps) { 81 + return <StarterPackAuthCheck routeParams={route.params} /> 82 + } 83 + 84 + export function StarterPackScreenShort({route}: StarterPackScreenShortProps) { 72 85 const {_} = useLingui() 86 + const { 87 + data: resolvedStarterPack, 88 + isLoading, 89 + isError, 90 + } = useResolvedStarterPackShortLink({ 91 + code: route.params.code, 92 + }) 93 + 94 + if (isLoading || isError || !resolvedStarterPack) { 95 + return ( 96 + <ListMaybePlaceholder 97 + isLoading={isLoading} 98 + isError={isError} 99 + errorMessage={_(msg`That starter pack could not be found.`)} 100 + emptyMessage={_(msg`That starter pack could not be found.`)} 101 + /> 102 + ) 103 + } 104 + return <StarterPackAuthCheck routeParams={resolvedStarterPack} /> 105 + } 106 + 107 + export function StarterPackAuthCheck({ 108 + routeParams, 109 + }: { 110 + routeParams: StarterPackScreeProps['route']['params'] 111 + }) { 112 + const navigation = useNavigation<NavigationProp>() 113 + const setActiveStarterPack = useSetActiveStarterPack() 73 114 const {currentAccount} = useSession() 74 115 75 - const {name, rkey} = route.params 116 + React.useEffect(() => { 117 + if (currentAccount) return 118 + 119 + const uri = createStarterPackUri({ 120 + did: routeParams.name, 121 + rkey: routeParams.rkey, 122 + }) 123 + 124 + if (!uri) return 125 + setActiveStarterPack({ 126 + uri, 127 + }) 128 + 129 + navigation.goBack() 130 + }, [routeParams, currentAccount, navigation, setActiveStarterPack]) 131 + 132 + if (!currentAccount) return null 133 + 134 + return <StarterPackScreenInner routeParams={routeParams} /> 135 + } 136 + 137 + export function StarterPackScreenInner({ 138 + routeParams, 139 + }: { 140 + routeParams: StarterPackScreeProps['route']['params'] 141 + }) { 142 + const {name, rkey} = routeParams 143 + const {_} = useLingui() 144 + const {currentAccount} = useSession() 145 + 76 146 const moderationOpts = useModerationOpts() 77 147 const { 78 148 data: did, ··· 113 183 } 114 184 115 185 return ( 116 - <StarterPackScreenInner 186 + <StarterPackScreenLoaded 117 187 starterPack={starterPack} 118 - routeParams={route.params} 188 + routeParams={routeParams} 119 189 listMembersQuery={listMembersQuery} 120 190 moderationOpts={moderationOpts} 121 191 /> 122 192 ) 123 193 } 124 194 125 - function StarterPackScreenInner({ 195 + function StarterPackScreenLoaded({ 126 196 starterPack, 127 197 routeParams, 128 198 listMembersQuery,
+24
src/state/queries/resolve-short-link.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + import {resolveShortLink} from 'lib/link-meta/resolve-short-link' 4 + import {parseStarterPackUri} from 'lib/strings/starter-pack' 5 + import {STALE} from 'state/queries/index' 6 + 7 + const ROOT_URI = 'https://go.bsky.app/' 8 + 9 + const RQKEY_ROOT = 'resolved-short-link' 10 + export const RQKEY = (code: string) => [RQKEY_ROOT, code] 11 + 12 + export function useResolvedStarterPackShortLink({code}: {code: string}) { 13 + return useQuery({ 14 + queryKey: RQKEY(code), 15 + queryFn: async () => { 16 + const uri = `${ROOT_URI}${code}` 17 + const res = await resolveShortLink(uri) 18 + return parseStarterPackUri(res) 19 + }, 20 + retry: 1, 21 + enabled: Boolean(code), 22 + staleTime: STALE.HOURS.ONE, 23 + }) 24 + }
+20
src/state/shell/logged-out.tsx
··· 50 50 const activeStarterPack = useActiveStarterPack() 51 51 const {hasSession} = useSession() 52 52 const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession 53 + 53 54 const [state, setState] = React.useState<State>({ 54 55 showLoggedOut: shouldShowStarterPack, 55 56 requestedAccountSwitchTo: shouldShowStarterPack ··· 58 59 : 'new' 59 60 : undefined, 60 61 }) 62 + 63 + const [prevActiveStarterPack, setPrevActiveStarterPack] = 64 + React.useState(activeStarterPack) 65 + if (activeStarterPack?.uri !== prevActiveStarterPack?.uri) { 66 + setPrevActiveStarterPack(activeStarterPack) 67 + if (activeStarterPack) { 68 + setState(s => ({ 69 + ...s, 70 + showLoggedOut: true, 71 + requestedAccountSwitchTo: 'starterpack', 72 + })) 73 + } else { 74 + setState(s => ({ 75 + ...s, 76 + showLoggedOut: false, 77 + requestedAccountSwitchTo: undefined, 78 + })) 79 + } 80 + } 61 81 62 82 const controls = React.useMemo<Controls>( 63 83 () => ({