mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react' 2import {Alert} from 'react-native' 3import * as Linking from 'expo-linking' 4 5import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 6import {logger} from '#/logger' 7import {isNative} from '#/platform/detection' 8import {useSession} from '#/state/session' 9import {useCloseAllActiveElements} from '#/state/util' 10import { 11 parseAgeAssuranceRedirectDialogState, 12 useAgeAssuranceRedirectDialogControl, 13} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 14import {useIntentDialogs} from '#/components/intents/IntentDialogs' 15import {Referrer} from '../../../modules/expo-bluesky-swiss-army' 16import {useApplyPullRequestOTAUpdate} from './useOTAUpdates' 17 18type IntentType = 'compose' | 'verify-email' | 'age-assurance' | 'apply-ota' 19 20const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/ 21 22// This needs to stay outside of react to persist between account switches 23let previousIntentUrl = '' 24 25export function useIntentHandler() { 26 const incomingUrl = Linking.useURL() 27 const composeIntent = useComposeIntent() 28 const verifyEmailIntent = useVerifyEmailIntent() 29 const ageAssuranceRedirectDialogControl = 30 useAgeAssuranceRedirectDialogControl() 31 const {currentAccount} = useSession() 32 const {tryApplyUpdate} = useApplyPullRequestOTAUpdate() 33 34 React.useEffect(() => { 35 const handleIncomingURL = (url: string) => { 36 const referrerInfo = Referrer.getReferrerInfo() 37 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { 38 logger.metric('deepLink:referrerReceived', { 39 to: url, 40 referrer: referrerInfo?.referrer, 41 hostname: referrerInfo?.hostname, 42 }) 43 } 44 45 // We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three 46 // slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care 47 // of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first 48 // path parameter is in pathname rather than in hostname. 49 if (url.startsWith('bluesky://') && !url.startsWith('bluesky:///')) { 50 url = url.replace('bluesky://', 'bluesky:///') 51 } 52 53 const urlp = new URL(url) 54 const [_, intent, intentType] = urlp.pathname.split('/') 55 56 // On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the 57 // intent check. On web, we have to check the first part of the path since we have an actual hostname 58 const isIntent = intent === 'intent' 59 const params = urlp.searchParams 60 61 if (!isIntent) return 62 63 switch (intentType as IntentType) { 64 case 'compose': { 65 composeIntent({ 66 text: params.get('text'), 67 imageUrisStr: params.get('imageUris'), 68 videoUri: params.get('videoUri'), 69 }) 70 return 71 } 72 case 'verify-email': { 73 const code = params.get('code') 74 if (!code) return 75 verifyEmailIntent(code) 76 return 77 } 78 case 'age-assurance': { 79 const state = parseAgeAssuranceRedirectDialogState({ 80 result: params.get('result') ?? undefined, 81 actorDid: params.get('actorDid') ?? undefined, 82 }) 83 84 /* 85 * If we don't have an account or the account doesn't match, do 86 * nothing. By the time the user switches to their other account, AA 87 * state should be ready for them. 88 */ 89 if ( 90 state && 91 currentAccount && 92 state.actorDid === currentAccount.did 93 ) { 94 ageAssuranceRedirectDialogControl.open(state) 95 } 96 return 97 } 98 case 'apply-ota': { 99 const channel = params.get('channel') 100 if (!channel) { 101 Alert.alert('Error', 'No channel provided to look for.') 102 } else { 103 tryApplyUpdate(channel) 104 } 105 } 106 default: { 107 return 108 } 109 } 110 } 111 112 if (incomingUrl) { 113 if (previousIntentUrl === incomingUrl) { 114 return 115 } 116 handleIncomingURL(incomingUrl) 117 previousIntentUrl = incomingUrl 118 } 119 }, [ 120 incomingUrl, 121 composeIntent, 122 verifyEmailIntent, 123 ageAssuranceRedirectDialogControl, 124 currentAccount, 125 tryApplyUpdate, 126 ]) 127} 128 129export function useComposeIntent() { 130 const closeAllActiveElements = useCloseAllActiveElements() 131 const {openComposer} = useOpenComposer() 132 const {hasSession} = useSession() 133 134 return React.useCallback( 135 ({ 136 text, 137 imageUrisStr, 138 videoUri, 139 }: { 140 text: string | null 141 imageUrisStr: string | null 142 videoUri: string | null 143 }) => { 144 if (!hasSession) return 145 closeAllActiveElements() 146 147 // Whenever a video URI is present, we don't support adding images right now. 148 if (videoUri) { 149 const [uri, width, height] = videoUri.split('|') 150 openComposer({ 151 text: text ?? undefined, 152 videoUri: {uri, width: Number(width), height: Number(height)}, 153 }) 154 return 155 } 156 157 const imageUris = imageUrisStr 158 ?.split(',') 159 .filter(part => { 160 // For some security, we're going to filter out any image uri that is external. We don't want someone to 161 // be able to provide some link like "bluesky://intent/compose?imageUris=https://IHaveYourIpNow.com/image.jpeg 162 // and we load that image 163 if (part.includes('https://') || part.includes('http://')) { 164 return false 165 } 166 // We also should just filter out cases that don't have all the info we need 167 return VALID_IMAGE_REGEX.test(part) 168 }) 169 .map(part => { 170 const [uri, width, height] = part.split('|') 171 return {uri, width: Number(width), height: Number(height)} 172 }) 173 174 setTimeout(() => { 175 openComposer({ 176 text: text ?? undefined, 177 imageUris: isNative ? imageUris : undefined, 178 }) 179 }, 500) 180 }, 181 [hasSession, closeAllActiveElements, openComposer], 182 ) 183} 184 185function useVerifyEmailIntent() { 186 const closeAllActiveElements = useCloseAllActiveElements() 187 const {verifyEmailDialogControl: control, setVerifyEmailState: setState} = 188 useIntentDialogs() 189 return React.useCallback( 190 (code: string) => { 191 closeAllActiveElements() 192 setState({ 193 code, 194 }) 195 setTimeout(() => { 196 control.open() 197 }, 1000) 198 }, 199 [closeAllActiveElements, control, setState], 200 ) 201}