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}