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