Bluesky app fork with some witchin' additions 馃挮
at main 179 lines 5.5 kB view raw
1import React from 'react' 2import {Alert} from 'react-native' 3import * as Linking from 'expo-linking' 4import * as WebBrowser from 'expo-web-browser' 5 6import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 7import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 8import {useSession} from '#/state/session' 9import {useCloseAllActiveElements} from '#/state/util' 10import {useIntentDialogs} from '#/components/intents/IntentDialogs' 11import {useAnalytics} from '#/analytics' 12import {IS_IOS, IS_NATIVE} from '#/env' 13import {Referrer} from '../../../modules/expo-bluesky-swiss-army' 14import {useApplyPullRequestOTAUpdate} from './useOTAUpdates' 15 16type IntentType = 'compose' | 'verify-email' | 'age-assurance' | 'apply-ota' 17 18const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/ 19 20// This needs to stay outside of react to persist between account switches 21let previousIntentUrl = '' 22 23export function useIntentHandler() { 24 const incomingUrl = Linking.useLinkingURL() 25 const ax = useAnalytics() 26 const composeIntent = useComposeIntent() 27 const verifyEmailIntent = useVerifyEmailIntent() 28 const {currentAccount} = useSession() 29 const {tryApplyUpdate} = useApplyPullRequestOTAUpdate() 30 31 React.useEffect(() => { 32 const handleIncomingURL = async (url: string) => { 33 if (IS_IOS) { 34 // Close in-app browser if it's open (iOS only) 35 await WebBrowser.dismissBrowser().catch(() => {}) 36 } 37 38 const referrerInfo = Referrer.getReferrerInfo() 39 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { 40 ax.metric('deepLink:referrerReceived', { 41 to: url, 42 referrer: referrerInfo?.referrer, 43 hostname: referrerInfo?.hostname, 44 }) 45 } 46 const urlp = parseLinkingUrl(url) 47 const [, intent, intentType] = urlp.pathname.split('/') 48 49 // On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the 50 // intent check. On web, we have to check the first part of the path since we have an actual hostname 51 const isIntent = intent === 'intent' 52 const params = urlp.searchParams 53 54 if (!isIntent) return 55 56 switch (intentType as IntentType) { 57 case 'compose': { 58 composeIntent({ 59 text: params.get('text'), 60 imageUrisStr: params.get('imageUris'), 61 videoUri: params.get('videoUri'), 62 }) 63 return 64 } 65 case 'verify-email': { 66 const code = params.get('code') 67 if (!code) return 68 verifyEmailIntent(code) 69 return 70 } 71 case 'age-assurance': { 72 // Handled in `#/ageAssurance/components/RedirectOverlay.tsx` 73 return 74 } 75 case 'apply-ota': { 76 const channel = params.get('channel') 77 if (!channel) { 78 Alert.alert('Error', 'No channel provided to look for.') 79 } else { 80 tryApplyUpdate(channel) 81 } 82 return 83 } 84 default: { 85 return 86 } 87 } 88 } 89 90 if (incomingUrl) { 91 if (previousIntentUrl === incomingUrl) { 92 return 93 } 94 handleIncomingURL(incomingUrl) 95 previousIntentUrl = incomingUrl 96 } 97 }, [ 98 incomingUrl, 99 ax, 100 composeIntent, 101 verifyEmailIntent, 102 currentAccount, 103 tryApplyUpdate, 104 ]) 105} 106 107export function useComposeIntent() { 108 const closeAllActiveElements = useCloseAllActiveElements() 109 const {openComposer} = useOpenComposer() 110 const {hasSession} = useSession() 111 112 return React.useCallback( 113 ({ 114 text, 115 imageUrisStr, 116 videoUri, 117 }: { 118 text: string | null 119 imageUrisStr: string | null 120 videoUri: string | null 121 }) => { 122 if (!hasSession) return 123 closeAllActiveElements() 124 125 // Whenever a video URI is present, we don't support adding images right now. 126 if (videoUri) { 127 const [uri, width, height] = videoUri.split('|') 128 openComposer({ 129 text: text ?? undefined, 130 videoUri: {uri, width: Number(width), height: Number(height)}, 131 }) 132 return 133 } 134 135 const imageUris = imageUrisStr 136 ?.split(',') 137 .filter(part => { 138 // For some security, we're going to filter out any image uri that is external. We don't want someone to 139 // be able to provide some link like "bluesky://intent/compose?imageUris=https://IHaveYourIpNow.com/image.jpeg 140 // and we load that image 141 if (part.includes('https://') || part.includes('http://')) { 142 return false 143 } 144 // We also should just filter out cases that don't have all the info we need 145 return VALID_IMAGE_REGEX.test(part) 146 }) 147 .map(part => { 148 const [uri, width, height] = part.split('|') 149 return {uri, width: Number(width), height: Number(height)} 150 }) 151 152 setTimeout(() => { 153 openComposer({ 154 text: text ?? undefined, 155 imageUris: IS_NATIVE ? imageUris : undefined, 156 }) 157 }, 500) 158 }, 159 [hasSession, closeAllActiveElements, openComposer], 160 ) 161} 162 163function useVerifyEmailIntent() { 164 const closeAllActiveElements = useCloseAllActiveElements() 165 const {verifyEmailDialogControl: control, setVerifyEmailState: setState} = 166 useIntentDialogs() 167 return React.useCallback( 168 (code: string) => { 169 closeAllActiveElements() 170 setState({ 171 code, 172 }) 173 setTimeout(() => { 174 control.open() 175 }, 1000) 176 }, 177 [closeAllActiveElements, control, setState], 178 ) 179}