mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import * as Linking from 'expo-linking'
3
4import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
5import {logEvent} from '#/lib/statsig/statsig'
6import {isNative} from '#/platform/detection'
7import {useSession} from '#/state/session'
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} = useOpenComposer()
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}