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