Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 181 lines 5.6 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 logContext: 'Deeplink', 132 }) 133 return 134 } 135 136 const imageUris = imageUrisStr 137 ?.split(',') 138 .filter(part => { 139 // For some security, we're going to filter out any image uri that is external. We don't want someone to 140 // be able to provide some link like "bluesky://intent/compose?imageUris=https://IHaveYourIpNow.com/image.jpeg 141 // and we load that image 142 if (part.includes('https://') || part.includes('http://')) { 143 return false 144 } 145 // We also should just filter out cases that don't have all the info we need 146 return VALID_IMAGE_REGEX.test(part) 147 }) 148 .map(part => { 149 const [uri, width, height] = part.split('|') 150 return {uri, width: Number(width), height: Number(height)} 151 }) 152 153 setTimeout(() => { 154 openComposer({ 155 text: text ?? undefined, 156 imageUris: IS_NATIVE ? imageUris : undefined, 157 logContext: 'Deeplink', 158 }) 159 }, 500) 160 }, 161 [hasSession, closeAllActiveElements, openComposer], 162 ) 163} 164 165function useVerifyEmailIntent() { 166 const closeAllActiveElements = useCloseAllActiveElements() 167 const {verifyEmailDialogControl: control, setVerifyEmailState: setState} = 168 useIntentDialogs() 169 return React.useCallback( 170 (code: string) => { 171 closeAllActiveElements() 172 setState({ 173 code, 174 }) 175 setTimeout(() => { 176 control.open() 177 }, 1000) 178 }, 179 [closeAllActiveElements, control, setState], 180 ) 181}