Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 181 lines 5.6 kB view raw
1import {useCallback, useEffect} 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 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 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 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}