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